diff --git a/Changelog.md b/Changelog.md index 6dd8d612..485dee9c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ ## v0.10.0 +- Full refactoring of `dolfinx.fem.petsc.NonlinearProblem`, which now uses the PETSc SNES backend. See [the non-linear poisson demo](./chapter2/nonlinpoisson_code.ipynb) for details. +- `dolfinx.fem.petsc.LinearProblem` now requires an additional argument, `petsc_options_prefix`. This should be a unique string identifier for each `LinearProblem` that is created. - Change how one reads in GMSH data with `gmshio`. See [the membrane code](./chapter1/membrane_code.ipynb) for more details. - `dolfinx.fem.FiniteElement.interpolation_points()` -> `dolfinx.fem.FiniteElement.interpolation_points`. diff --git a/README.md b/README.md index 4a3c3512..1d2465b2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Alternatively, if you want to add a separate chapter, a Jupyter notebook can be Any code added to the tutorial should work in parallel. If any changes are made to `ipynb` files, please ensure that these changes are reflected in the corresponding `py` files by using [`jupytext`](https://jupytext.readthedocs.io/en/latest/faq.html#can-i-use-jupytext-with-jupyterhub-binder-nteract-colab-saturn-or-azure): ```bash -python3 -m jupytext --sync */*.ipynb +python3 -m jupytext --sync */*.ipynb --set-formats ipynb,py:light ``` Any code added to the tutorial should work in parallel. @@ -61,4 +61,3 @@ from the root of this repository, and run ``` from the main directory. - diff --git a/_config.yml b/_config.yml index ca36bfc8..2fa2f3a1 100644 --- a/_config.yml +++ b/_config.yml @@ -68,3 +68,4 @@ html: exclude_patterns: [README.md, chapter2/advdiffreac.md] +only_build_toc_files: true diff --git a/chapter1/complex_mode.ipynb b/chapter1/complex_mode.ipynb index d2c8e79f..4a8e5011 100644 --- a/chapter1/complex_mode.ipynb +++ b/chapter1/complex_mode.ipynb @@ -11,7 +11,7 @@ "\n", "Many PDEs, such as the [Helmholtz equation](https://docs.fenicsproject.org/dolfinx/v0.4.1/python/demos/demo_helmholtz.html) require complex-valued fields.\n", "\n", - "For simplicity, let us consider a Poisson equation of the form: \n", + "For simplicity, let us consider a Poisson equation of the form:\n", "\n", "$$-\\Delta u = f \\text{ in } \\Omega,$$\n", "$$ f = -1 - 2j \\text{ in } \\Omega,$$\n", @@ -47,12 +47,13 @@ "from mpi4py import MPI\n", "import dolfinx\n", "import numpy as np\n", + "\n", "mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)\n", "V = dolfinx.fem.functionspace(mesh, (\"Lagrange\", 1))\n", - "u_r = dolfinx.fem.Function(V, dtype=np.float64) \n", + "u_r = dolfinx.fem.Function(V, dtype=np.float64)\n", "u_r.interpolate(lambda x: x[0])\n", "u_c = dolfinx.fem.Function(V, dtype=np.complex128)\n", - "u_c.interpolate(lambda x:0.5*x[0]**2 + 1j*x[1]**2)\n", + "u_c.interpolate(lambda x: 0.5 * x[0] ** 2 + 1j * x[1] ** 2)\n", "print(u_r.x.array.dtype)\n", "print(u_c.x.array.dtype)" ] @@ -78,8 +79,9 @@ "source": [ "from petsc4py import PETSc\n", "from dolfinx.fem.petsc import assemble_vector\n", + "\n", "print(PETSc.ScalarType)\n", - "assert np.dtype(PETSc.ScalarType).kind == 'c'" + "assert np.dtype(PETSc.ScalarType).kind == \"c\"" ] }, { @@ -99,6 +101,7 @@ "outputs": [], "source": [ "import ufl\n", + "\n", "u = ufl.TrialFunction(V)\n", "v = ufl.TestFunction(V)\n", "f = dolfinx.fem.Constant(mesh, PETSc.ScalarType(-1 - 2j))\n", @@ -167,11 +170,15 @@ "metadata": {}, "outputs": [], "source": [ - "mesh.topology.create_connectivity(mesh.topology.dim-1, mesh.topology.dim)\n", + "mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim)\n", "boundary_facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)\n", - "boundary_dofs = dolfinx.fem.locate_dofs_topological(V, mesh.topology.dim-1, boundary_facets)\n", + "boundary_dofs = dolfinx.fem.locate_dofs_topological(\n", + " V, mesh.topology.dim - 1, boundary_facets\n", + ")\n", "bc = dolfinx.fem.dirichletbc(u_c, boundary_dofs)\n", - "problem = dolfinx.fem.petsc.LinearProblem(a, L, bcs=[bc])\n", + "problem = dolfinx.fem.petsc.LinearProblem(\n", + " a, L, bcs=[bc], petsc_options_prefix=\"complex_poisson\"\n", + ")\n", "uh = problem.solve()" ] }, @@ -193,11 +200,13 @@ "outputs": [], "source": [ "x = ufl.SpatialCoordinate(mesh)\n", - "u_ex = 0.5 * x[0]**2 + 1j*x[1]**2\n", - "L2_error = dolfinx.fem.form(ufl.dot(uh-u_ex, uh-u_ex) * ufl.dx(metadata={\"quadrature_degree\": 5}))\n", + "u_ex = 0.5 * x[0] ** 2 + 1j * x[1] ** 2\n", + "L2_error = dolfinx.fem.form(\n", + " ufl.dot(uh - u_ex, uh - u_ex) * ufl.dx(metadata={\"quadrature_degree\": 5})\n", + ")\n", "local_error = dolfinx.fem.assemble_scalar(L2_error)\n", "global_error = np.sqrt(mesh.comm.allreduce(local_error, op=MPI.SUM))\n", - "max_error = mesh.comm.allreduce(np.max(np.abs(u_c.x.array-uh.x.array)))\n", + "max_error = mesh.comm.allreduce(np.max(np.abs(u_c.x.array - uh.x.array)))\n", "print(global_error, max_error)" ] }, @@ -219,38 +228,23 @@ "outputs": [], "source": [ "import pyvista\n", - "pyvista.start_xvfb()\n", + "\n", + "pyvista.start_xvfb(0.1)\n", "mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim)\n", "p_mesh = pyvista.UnstructuredGrid(*dolfinx.plot.vtk_mesh(mesh, mesh.topology.dim))\n", "pyvista_cells, cell_types, geometry = dolfinx.plot.vtk_mesh(V)\n", "grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry)\n", "grid.point_data[\"u_real\"] = uh.x.array.real\n", "grid.point_data[\"u_imag\"] = uh.x.array.imag\n", - "_ = grid.set_active_scalars(\"u_real\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16", - "metadata": {}, - "outputs": [], - "source": [ + "_ = grid.set_active_scalars(\"u_real\")\n", + "\n", "p_real = pyvista.Plotter()\n", "p_real.add_text(\"uh real\", position=\"upper_edge\", font_size=14, color=\"black\")\n", "p_real.add_mesh(grid, show_edges=True)\n", "p_real.view_xy()\n", "if not pyvista.OFF_SCREEN:\n", - " p_real.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17", - "metadata": {}, - "outputs": [], - "source": [ + " p_real.show()\n", + "\n", "grid.set_active_scalars(\"u_imag\")\n", "p_imag = pyvista.Plotter()\n", "p_imag.add_text(\"uh imag\", position=\"upper_edge\", font_size=14, color=\"black\")\n", @@ -259,14 +253,6 @@ "if not pyvista.OFF_SCREEN:\n", " p_imag.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/chapter1/complex_mode.py b/chapter1/complex_mode.py index 65981976..ed4b88c1 100644 --- a/chapter1/complex_mode.py +++ b/chapter1/complex_mode.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (DOLFINx complex) # language: python @@ -19,7 +19,7 @@ # # Many PDEs, such as the [Helmholtz equation](https://docs.fenicsproject.org/dolfinx/v0.4.1/python/demos/demo_helmholtz.html) require complex-valued fields. # -# For simplicity, let us consider a Poisson equation of the form: +# For simplicity, let us consider a Poisson equation of the form: # # $$-\Delta u = f \text{ in } \Omega,$$ # $$ f = -1 - 2j \text{ in } \Omega,$$ @@ -45,17 +45,20 @@ # FEniCSx supports both real and complex numbers, so we can create a function space with real valued or complex valued coefficients. # +# + from mpi4py import MPI import dolfinx import numpy as np + mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10) V = dolfinx.fem.functionspace(mesh, ("Lagrange", 1)) -u_r = dolfinx.fem.Function(V, dtype=np.float64) +u_r = dolfinx.fem.Function(V, dtype=np.float64) u_r.interpolate(lambda x: x[0]) u_c = dolfinx.fem.Function(V, dtype=np.complex128) -u_c.interpolate(lambda x:0.5*x[0]**2 + 1j*x[1]**2) +u_c.interpolate(lambda x: 0.5 * x[0] ** 2 + 1j * x[1] ** 2) print(u_r.x.array.dtype) print(u_c.x.array.dtype) +# - # However, as we would like to solve linear algebra problems of the form $Ax=b$, we need to be able to use matrices and vectors that support real and complex numbers. As [PETSc](https://petsc.org/release/) is one of the most popular interfaces to linear algebra packages, we need to be able to work with their matrix and vector structures. # @@ -63,20 +66,26 @@ # # We check that we are using the correct installation of PETSc by inspecting the scalar type. +# + from petsc4py import PETSc from dolfinx.fem.petsc import assemble_vector + print(PETSc.ScalarType) -assert np.dtype(PETSc.ScalarType).kind == 'c' +assert np.dtype(PETSc.ScalarType).kind == "c" +# - # ## Variational problem # We are now ready to define our variational problem +# + import ufl + u = ufl.TrialFunction(V) v = ufl.TestFunction(V) f = dolfinx.fem.Constant(mesh, PETSc.ScalarType(-1 - 2j)) a = ufl.inner(ufl.grad(u), ufl.grad(v)) * ufl.dx L = ufl.inner(f, v) * ufl.dx +# - # Note that we have used the `PETSc.ScalarType` to wrap the constant source on the right hand side. This is because we want the integration kernels to assemble into the correct floating type. # @@ -99,11 +108,15 @@ # We define our Dirichlet condition and setup and solve the variational problem. # ## Solve variational problem -mesh.topology.create_connectivity(mesh.topology.dim-1, mesh.topology.dim) +mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim) boundary_facets = dolfinx.mesh.exterior_facet_indices(mesh.topology) -boundary_dofs = dolfinx.fem.locate_dofs_topological(V, mesh.topology.dim-1, boundary_facets) +boundary_dofs = dolfinx.fem.locate_dofs_topological( + V, mesh.topology.dim - 1, boundary_facets +) bc = dolfinx.fem.dirichletbc(u_c, boundary_dofs) -problem = dolfinx.fem.petsc.LinearProblem(a, L, bcs=[bc]) +problem = dolfinx.fem.petsc.LinearProblem( + a, L, bcs=[bc], petsc_options_prefix="complex_poisson" +) uh = problem.solve() # We compute the $L^2$ error and the max error. @@ -112,11 +125,13 @@ # x = ufl.SpatialCoordinate(mesh) -u_ex = 0.5 * x[0]**2 + 1j*x[1]**2 -L2_error = dolfinx.fem.form(ufl.dot(uh-u_ex, uh-u_ex) * ufl.dx(metadata={"quadrature_degree": 5})) +u_ex = 0.5 * x[0] ** 2 + 1j * x[1] ** 2 +L2_error = dolfinx.fem.form( + ufl.dot(uh - u_ex, uh - u_ex) * ufl.dx(metadata={"quadrature_degree": 5}) +) local_error = dolfinx.fem.assemble_scalar(L2_error) global_error = np.sqrt(mesh.comm.allreduce(local_error, op=MPI.SUM)) -max_error = mesh.comm.allreduce(np.max(np.abs(u_c.x.array-uh.x.array))) +max_error = mesh.comm.allreduce(np.max(np.abs(u_c.x.array - uh.x.array))) print(global_error, max_error) # ## Plotting @@ -124,8 +139,10 @@ # Finally, we plot the real and imaginary solutions. # +# + import pyvista -pyvista.start_xvfb() + +pyvista.start_xvfb(0.1) mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim) p_mesh = pyvista.UnstructuredGrid(*dolfinx.plot.vtk_mesh(mesh, mesh.topology.dim)) pyvista_cells, cell_types, geometry = dolfinx.plot.vtk_mesh(V) @@ -148,5 +165,3 @@ p_imag.view_xy() if not pyvista.OFF_SCREEN: p_imag.show() - - diff --git a/chapter1/fundamentals_code.ipynb b/chapter1/fundamentals_code.ipynb index a9c0c12e..a18c38b5 100644 --- a/chapter1/fundamentals_code.ipynb +++ b/chapter1/fundamentals_code.ipynb @@ -25,24 +25,24 @@ "```\n", "\n", "The Poisson problem has so far featured a general domain $\\Omega$ and general functions $u_D$ for the boundary conditions and $f$ for the right hand side.\n", - "Therefore, we need to make specific choices of $\\Omega, u_D$ and $f$. A wise choice is to construct a problem with a known analytical solution, so that we can check that the computed solution is correct. The primary candidates are lower-order polynomials. The continuous Galerkin finite element spaces of degree $r$ will exactly reproduce polynomials of degree $r$. \n", - "\n", " We use this fact to construct a quadratic function in $2D$. In particular we choose\n", "\\begin{align}\n", " u_e(x,y)=1+x^2+2y^2\n", " \\end{align}\n", "\n", - "Inserting $u_e$ in the original boundary problem, we find that \n", + "Inserting $u_e$ in the original boundary problem, we find that\n", "\\begin{align}\n", " f(x,y)= -6,\\qquad u_D(x,y)=u_e(x,y)=1+x^2+2y^2,\n", "\\end{align}\n", - "regardless of the shape of the domain as long as we prescribe \n", + "regardless of the shape of the domain as long as we prescribe\n", "$u_e$ on the boundary.\n", "\n", "For simplicity, we choose the domain to be a unit square $\\Omega=[0,1]\\times [0,1]$\n", "\n", - "This simple but very powerful method for constructing test problems is called _the method of manufactured solutions_. \n", + "This simple but very powerful method for constructing test problems is called _the method of manufactured solutions_.\n", "First pick a simple expression for the exact solution, plug into\n", "the equation to obtain the right-hand side (source term $f$). Then solve the equation with this right hand side, and using the exact solution as boundary condition. Finally, we create a program that tries to reproduce the exact solution.\n", "\n", @@ -67,29 +67,27 @@ { "cell_type": "code", "execution_count": null, - "id": "2", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "d0b956ef", + "metadata": {}, "outputs": [], "source": [ "from mpi4py import MPI\n", "from dolfinx import mesh\n", + "import numpy\n", + "\n", "domain = mesh.create_unit_square(MPI.COMM_WORLD, 8, 8, mesh.CellType.quadrilateral)" ] }, { "cell_type": "markdown", - "id": "3", + "id": "ae54745d", "metadata": {}, "source": [ - "Note that in addition to give how many elements we would like to have in each direction. \n", - "We also have to supply the _MPI-communicator_. \n", - "This is to specify how we would like the program to behave in parallel. \n", - "If we supply `MPI.COMM_WORLD` we create a single mesh, whose data is distributed over the number of processors we \n", - "would like to use. We can for instance run the program in parallel on two processors by using `mpirun`, as: \n", + "Note that in addition to give how many elements we would like to have in each direction.\n", + "We also have to supply the _MPI-communicator_.\n", + "This is to specify how we would like the program to behave in parallel.\n", + "If we supply `MPI.COMM_WORLD` we create a single mesh, whose data is distributed over the number of processors we\n", + "would like to use. We can for instance run the program in parallel on two processors by using `mpirun`, as:\n", "``` bash\n", " mpirun -n 2 python3 t1.py\n", "```\n", @@ -104,57 +102,68 @@ { "cell_type": "code", "execution_count": null, - "id": "4", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "f1645ffa", + "metadata": {}, "outputs": [], "source": [ - "from dolfinx.fem import functionspace\n", - "V = functionspace(domain, (\"Lagrange\", 1))" + "from dolfinx import fem\n", + "\n", + "V = fem.functionspace(domain, (\"Lagrange\", 1))" + ] + }, + { + "cell_type": "markdown", + "id": "0293ff94", + "metadata": {}, + "source": [ + "## Dirichlet boundary conditions\n", + "Next, we create a function that will hold the Dirichlet boundary data, and use interpolation to\n", + "fill it with the appropriate data." ] }, { "cell_type": "code", "execution_count": null, - "id": "5", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "39113935", + "metadata": {}, "outputs": [], "source": [ - "from dolfinx import fem\n", "uD = fem.Function(V)\n", - "uD.interpolate(lambda x: 1 + x[0]**2 + 2 * x[1]**2)" + "uD.interpolate(lambda x: 1 + x[0] ** 2 + 2 * x[1] ** 2)" ] }, { "cell_type": "markdown", - "id": "6", + "id": "b9618de3", "metadata": {}, "source": [ - "We now have the boundary data (and in this case the solution of \n", - "the finite element problem) represented in the discrete function space.\n", + "We now have the boundary data (and in this case the solution of the finite element problem) represented in the discrete function space.\n", "Next we would like to apply the boundary values to all degrees of freedom that are on the boundary of the discrete domain. We start by identifying the facets (line-segments) representing the outer boundary, using `dolfinx.mesh.exterior_facet_indices`." ] }, + { + "cell_type": "markdown", + "id": "5d36dd62", + "metadata": { + "magic_args": "vscode={\"languageId\": \"python\"}" + }, + "source": [] + }, + { + "cell_type": "markdown", + "id": "c2728aba", + "metadata": {}, + "source": [ + "Create facet to cell connectivity required to determine boundary facets" + ] + }, { "cell_type": "code", "execution_count": null, "id": "7", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ - "import numpy\n", - "# Create facet to cell connectivity required to determine boundary facets\n", "tdim = domain.topology.dim\n", "fdim = tdim - 1\n", "domain.topology.create_connectivity(fdim, tdim)\n", @@ -164,13 +173,15 @@ { "cell_type": "markdown", "id": "8", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ - "For the current problem, as we are using the \"Lagrange\" 1 function space, the degrees of freedom are located at the vertices of each cell, thus each facet contains two degrees of freedom. \n", + "For the current problem, as we are using the \"Lagrange\" 1 function space, the degrees of freedom are located at the vertices of each cell, thus each facet contains two degrees of freedom.\n", "\n", - "To find the local indices of these degrees of freedom, we use `dolfinx.fem.locate_dofs_topological`, which takes in the function space, the dimension of entities in the mesh we would like to identify and the local entities. \n", + "To find the local indices of these degrees of freedom, we use `dolfinx.fem.locate_dofs_topological`, which takes in the function space, the dimension of entities in the mesh we would like to identify and the local entities.\n", "```{admonition} Local ordering of degrees of freedom and mesh vertices\n", - "Many people expect there to be a 1-1 correspondence between the mesh coordinates and the coordinates of the degrees of freedom. \n", + "Many people expect there to be a 1-1 correspondence between the mesh coordinates and the coordinates of the degrees of freedom.\n", "However, this is only true in the case of `Lagrange` 1 elements on a first order mesh. Therefore, in DOLFINx we use separate local numbering for the mesh coordinates and the dof coordinates. To obtain the local dof coordinates we can use `V.tabulate_dof_coordinates()`, while the ordering of the local vertices can be obtained by `mesh.geometry.x`.\n", "```\n", "With this data at hand, we can create the Dirichlet boundary condition" @@ -180,11 +191,7 @@ "cell_type": "code", "execution_count": null, "id": "9", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ "boundary_dofs = fem.locate_dofs_topological(V, fdim, boundary_facets)\n", @@ -194,7 +201,9 @@ { "cell_type": "markdown", "id": "10", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "## Defining the trial and test function\n", "\n", @@ -207,15 +216,14 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "4d981ec5", "metadata": { - "vscode": { - "languageId": "python" - } + "lines_to_next_cell": 0 }, "outputs": [], "source": [ "import ufl\n", + "\n", "u = ufl.TrialFunction(V)\n", "v = ufl.TestFunction(V)" ] @@ -226,21 +234,18 @@ "metadata": {}, "source": [ "## Defining the source term\n", - "As the source term is constant over the domain, we use `dolfinx.Constant`" + "As the source term is constant over the domain, we use `dolfinx.fem.Constant`" ] }, { "cell_type": "code", "execution_count": null, - "id": "13", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "4a0c8acb", + "metadata": {}, "outputs": [], "source": [ "from dolfinx import default_scalar_type\n", + "\n", "f = fem.Constant(domain, default_scalar_type(-6))" ] }, @@ -250,8 +255,10 @@ "metadata": {}, "source": [ "```{admonition} Compilation speed-up\n", - "Instead of wrapping $-6$ in a `dolfinx.Constant`, we could simply define $f$ as `f=-6`.\n", - "However, if we would like to change this parameter later in the simulation, we would have to redefine our variational formulation. The `dolfinx.Constant` allows us to update the value in $f$ by using `f.value=5`. Additionally, by indicating that $f$ is a constant, we speed of compilation of the variational formulations required for the created linear system. \n", + "Instead of wrapping $-6$ in a `dolfinx.fem.Constant`, we could simply define $f$ as `f=-6`.\n", + "However, if we would like to change this parameter later in the simulation, we would have to redefine our variational formulation.\n", + "The `dolfinx.fem.Constant` allows us to update the value in $f$ by using `f.value=5`.\n", + "Additionally, by indicating that $f$ is a constant, we speed of compilation of the variational formulations required for the created linear system.\n", "```\n", "## Defining the variational problem\n", "As we now have defined all variables used to describe our variational problem, we can create the weak formulation" @@ -261,11 +268,7 @@ "cell_type": "code", "execution_count": null, "id": "15", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ "a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx\n", @@ -278,13 +281,17 @@ "metadata": {}, "source": [ "Note that there is a very close correspondence between the Python syntax and the mathematical syntax\n", - "$\\int_{\\Omega} \\nabla u \\cdot \\nabla v ~\\mathrm{d} x$ and $\\int_{\\Omega}fv~\\mathrm{d} x$. \n", + "$\\int_{\\Omega} \\nabla u \\cdot \\nabla v ~\\mathrm{d} x$ and $\\int_{\\Omega}fv~\\mathrm{d} x$.\n", "The integration over the domain $\\Omega$ is defined by using `ufl.dx`, an integration measure over all cells of the mesh.\n", "\n", "This is the key strength of FEniCSx: the formulas in the variational formulation translate directly to very similar Python code, a feature that makes it easy to specify and solve complicated PDE problems.\n", "\n", "## Expressing inner products\n", - "The inner product $\\int_\\Omega \\nabla u \\cdot \\nabla v ~\\mathrm{d} x$ can be expressed in various ways in UFL. We have used the notation `ufl.dot(ufl.grad(u), ufl.grad(v))*ufl.dx`. The dot product in UFL computes the sum (contraction) over the last index of the first factor and first index of the second factor. In this case, both factors are tensors of rank one (vectors) and so the sum is just over the single index of both $\\nabla u$ and $\\nabla v$. To compute an inner product of matrices (with two indices), one must instead of `ufl.dot` use the function `ufl.inner`. For vectors, `ufl.dot` and `ufl.inner` are equivalent.\n", + "The inner product $\\int_\\Omega \\nabla u \\cdot \\nabla v ~\\mathrm{d} x$ can be expressed in various ways in UFL. We have used the notation `ufl.dot(ufl.grad(u), ufl.grad(v))*ufl.dx`.\n", + "The dot product in UFL computes the sum (contraction) over the last index of the first factor and first index of the second factor.\n", + "In this case, both factors are tensors of rank one (vectors) and so the sum is just over the single index of both $\\nabla u$ and $\\nabla v$.\n", + "To compute an inner product of matrices (with two indices), on must use use the function `ufl.inner` instead of `ufl.dot`.\n", + "For vectors, `ufl.dot` and `ufl.inner` are equivalent.\n", "\n", "```{admonition} Complex numbers\n", "In DOLFINx, one can solve complex number problems by using an installation of PETSc using complex numbers.\n", @@ -296,25 +303,37 @@ "\n", "## Forming and solving the linear system\n", "\n", - "Having defined the finite element variational problem and boundary condition, we can create our `dolfinx.fem.petsc.LinearProblem`, as class for solving \n", + "Having defined the finite element variational problem and boundary condition, we can create our `dolfinx.fem.petsc.LinearProblem`, as class for solving\n", "the variational problem: Find $u_h\\in V$ such that $a(u_h, v)==L(v) \\quad \\forall v \\in \\hat{V}$. We will use PETSc as our linear algebra backend, using a direct solver (LU-factorization).\n", "See the [PETSc-documentation](https://petsc.org/main/docs/manual/ksp/?highlight=ksp#ksp-linear-system-solvers) of the method for more information.\n", - "PETSc is not a required dependency of DOLFINx, and therefore we explicitly import the DOLFINx wrapper for interfacing with PETSc." + "PETSc is not a required dependency of DOLFINx, and therefore we explicitly import the DOLFINx wrapper for interfacing with PETSc.\n", + "To ensure that the options passed to the LinearProblem is only used for the given KSP solver, we pass a **unique** option prefix as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e004fb0f", + "metadata": {}, + "outputs": [], + "source": [ + "from dolfinx.fem.petsc import LinearProblem" ] }, { "cell_type": "code", "execution_count": null, "id": "17", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ - "from dolfinx.fem.petsc import LinearProblem\n", - "problem = LinearProblem(a, L, bcs=[bc], petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=[bc],\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"Poisson\",\n", + ")\n", "uh = problem.solve()" ] }, @@ -325,23 +344,20 @@ "source": [ "Using `problem.solve()` we solve the linear system of equations and return a `dolfinx.fem.Function` containing the solution.\n", "## Computing the error\n", - "Finally, we want to compute the error to check the accuracy of the solution. We do this by comparing the finite element solution `u` with the exact solution. We do this by interpolating the exact solution into the the $P_2$-function space." + "Finally, we want to compute the error to check the accuracy of the solution. We do this by comparing the finite element solution `u` with the exact solution.\n", + "We do this by interpolating the exact solution into the the $P_2$-function space." ] }, { "cell_type": "code", "execution_count": null, "id": "19", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ "V2 = fem.functionspace(domain, (\"Lagrange\", 2))\n", "uex = fem.Function(V2)\n", - "uex.interpolate(lambda x: 1 + x[0]**2 + 2 * x[1]**2)" + "uex.interpolate(lambda x: 1 + x[0] ** 2 + 2 * x[1] ** 2)" ] }, { @@ -349,7 +365,9 @@ "id": "20", "metadata": {}, "source": [ - "We compute the error in two different ways. First, we compute the $L^2$-norm of the error, defined by $E=\\sqrt{\\int_\\Omega (u_D-u_h)^2\\mathrm{d} x}$. We use UFL to express the $L^2$-error, and use `dolfinx.fem.assemble_scalar` to compute the scalar value. In DOLFINx, `assemble_scalar` only assembles over the cells on the local process. This means that if we use 2 processes to solve our problem, we need to gather the solution to one (or all the processes.\n", + "We compute the error in two different ways. First, we compute the $L^2$-norm of the error, defined by $E=\\sqrt{\\int_\\Omega (u_D-u_h)^2\\mathrm{d} x}$.\n", + "We use UFL to express the $L^2$-error, and use `dolfinx.fem.assemble_scalar` to compute the scalar value.\n", + "In DOLFINx, `assemble_scalar` only assembles over the cells on the local process. This means that if we use 2 processes to solve our problem, we need to gather the solution to one (or all the processes.\n", "We can do this with the `MPI.allreduce` function." ] }, @@ -357,11 +375,7 @@ "cell_type": "code", "execution_count": null, "id": "21", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ "L2_error = fem.form(ufl.inner(uh - uex, uh - uex) * ufl.dx)\n", @@ -379,21 +393,17 @@ "$ u = \\sum_{j=1}^N U_j\\phi_j.$\n", "By writing `problem.solve()` we compute all the coefficients $U_1,\\dots, U_N$. These values are known as the _degrees of freedom_ (dofs). We can access the degrees of freedom by accessing the underlying vector in `uh`.\n", "However, as a second order function space has more dofs than a linear function space, we cannot compare these arrays directly.\n", - "As we allready have interpolated the exact solution into the first order space when creating the boundary condition, we can compare the maximum values at any degree of freedom of the approximation space." + "As we already have interpolated the exact solution into the first order space when creating the boundary condition, we can compare the maximum values at any degree of freedom of the approximation space." ] }, { "cell_type": "code", "execution_count": null, "id": "23", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "metadata": {}, "outputs": [], "source": [ - "error_max = numpy.max(numpy.abs(uD.x.array-uh.x.array))\n", + "error_max = numpy.max(numpy.abs(uD.x.array - uh.x.array))\n", "# Only print the error on one process\n", "if domain.comm.rank == 0:\n", " print(f\"Error_L2 : {error_L2:.2e}\")\n", @@ -415,27 +425,25 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "8e110fd6", "metadata": {}, "outputs": [], "source": [ "import pyvista\n", - "print(pyvista.global_theme.jupyter_backend)" + "\n", + "print(pyvista.global_theme.jupyter_backend)\n", + "pyvista.start_xvfb(0.1)" ] }, { "cell_type": "code", "execution_count": null, - "id": "26", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "25", + "metadata": {}, "outputs": [], "source": [ "from dolfinx import plot\n", - "pyvista.start_xvfb()\n", + "\n", "domain.topology.create_connectivity(tdim, tdim)\n", "topology, cell_types, geometry = plot.vtk_mesh(domain, tdim)\n", "grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)" @@ -443,15 +451,17 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "608648d0", "metadata": {}, "source": [ - "There are several backends that can be used with pyvista, and they have different benefits and drawbacks. See the [pyvista documentation](https://docs.pyvista.org/user-guide/jupyter/index.html#state-of-3d-interactive-jupyterlab-plotting) for more information and installation details. In this example and the rest of the tutorial we will use [panel](https://github.com/holoviz/panel)." + "There are several backends that can be used with pyvista, and they have different benefits and drawbacks.\n", + "See the [pyvista documentation](https://docs.pyvista.org/user-guide/jupyter/index.html#state-of-3d-interactive-jupyterlab-plotting) for more information and installation details.\n", + "n this example and the rest of the tutorial we will use [panel](https://github.com/holoviz/panel)." ] }, { "cell_type": "markdown", - "id": "28", + "id": "fbd6824e", "metadata": {}, "source": [ "We can now use the `pyvista.Plotter` to visualize the mesh. We visualize it by showing it in 2D and warped in 3D.\n", @@ -461,12 +471,8 @@ { "cell_type": "code", "execution_count": null, - "id": "29", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "8002054d", + "metadata": {}, "outputs": [], "source": [ "plotter = pyvista.Plotter()\n", @@ -480,7 +486,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "201f6a29", "metadata": {}, "source": [ "## Plotting a function using pyvista\n", @@ -490,12 +496,8 @@ { "cell_type": "code", "execution_count": null, - "id": "31", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "a1a5d9fa", + "metadata": {}, "outputs": [], "source": [ "u_topology, u_cell_types, u_geometry = plot.vtk_mesh(V)" @@ -503,7 +505,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "d805a262", "metadata": {}, "source": [ "Next, we create the `pyvista.UnstructuredGrid` and add the dof-values to the mesh." @@ -512,12 +514,8 @@ { "cell_type": "code", "execution_count": null, - "id": "33", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "6e3dcf4c", + "metadata": {}, "outputs": [], "source": [ "u_grid = pyvista.UnstructuredGrid(u_topology, u_cell_types, u_geometry)\n", @@ -532,7 +530,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "1c0a0e74", "metadata": {}, "source": [ "We can also warp the mesh by scalar to make use of the 3D plotting." @@ -541,12 +539,8 @@ { "cell_type": "code", "execution_count": null, - "id": "35", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "4c8ecea3", + "metadata": {}, "outputs": [], "source": [ "warped = u_grid.warp_by_scalar()\n", @@ -558,7 +552,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "d917e52a", "metadata": {}, "source": [ "## External post-processing\n", @@ -568,16 +562,13 @@ { "cell_type": "code", "execution_count": null, - "id": "37", - "metadata": { - "vscode": { - "languageId": "python" - } - }, + "id": "262fcf2c", + "metadata": {}, "outputs": [], "source": [ "from dolfinx import io\n", "from pathlib import Path\n", + "\n", "results_folder = Path(\"results\")\n", "results_folder.mkdir(exist_ok=True, parents=True)\n", "filename = results_folder / \"fundamentals\"\n", @@ -590,7 +581,7 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "7d1c6af3", "metadata": {}, "source": [ "```{bibliography}\n", @@ -601,7 +592,7 @@ ], "metadata": { "jupytext": { - "formats": "ipynb,py" + "formats": "ipynb,py:light" }, "kernelspec": { "display_name": "Python 3 (ipykernel)", diff --git a/chapter1/fundamentals_code.py b/chapter1/fundamentals_code.py index 7670ef10..9182cd1c 100644 --- a/chapter1/fundamentals_code.py +++ b/chapter1/fundamentals_code.py @@ -1,12 +1,12 @@ # --- # jupyter: # jupytext: -# formats: ipynb,py +# formats: ipynb,py:light # text_representation: # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -33,24 +33,24 @@ # ``` # # The Poisson problem has so far featured a general domain $\Omega$ and general functions $u_D$ for the boundary conditions and $f$ for the right hand side. -# Therefore, we need to make specific choices of $\Omega, u_D$ and $f$. A wise choice is to construct a problem with a known analytical solution, so that we can check that the computed solution is correct. The primary candidates are lower-order polynomials. The continuous Galerkin finite element spaces of degree $r$ will exactly reproduce polynomials of degree $r$. -# # We use this fact to construct a quadratic function in $2D$. In particular we choose # \begin{align} # u_e(x,y)=1+x^2+2y^2 # \end{align} # -# Inserting $u_e$ in the original boundary problem, we find that +# Inserting $u_e$ in the original boundary problem, we find that # \begin{align} # f(x,y)= -6,\qquad u_D(x,y)=u_e(x,y)=1+x^2+2y^2, # \end{align} -# regardless of the shape of the domain as long as we prescribe +# regardless of the shape of the domain as long as we prescribe # $u_e$ on the boundary. # # For simplicity, we choose the domain to be a unit square $\Omega=[0,1]\times [0,1]$ # -# This simple but very powerful method for constructing test problems is called _the method of manufactured solutions_. +# This simple but very powerful method for constructing test problems is called _the method of manufactured solutions_. # First pick a simple expression for the exact solution, plug into # the equation to obtain the right-hand side (source term $f$). Then solve the equation with this right hand side, and using the exact solution as boundary condition. Finally, we create a program that tries to reproduce the exact solution. # @@ -65,17 +65,19 @@ # ## Generating simple meshes # The next step is to define the discrete domain, _the mesh_. We do this by importing one of the built-in mesh generators. We will build a unit square mesh, i.e. a mesh spanning $[0,1]\times[0,1]$. It can consist of either triangles or quadrilaterals. -# + vscode={"languageId": "python"} +# + from mpi4py import MPI from dolfinx import mesh +import numpy + domain = mesh.create_unit_square(MPI.COMM_WORLD, 8, 8, mesh.CellType.quadrilateral) # - -# Note that in addition to give how many elements we would like to have in each direction. -# We also have to supply the _MPI-communicator_. -# This is to specify how we would like the program to behave in parallel. -# If we supply `MPI.COMM_WORLD` we create a single mesh, whose data is distributed over the number of processors we -# would like to use. We can for instance run the program in parallel on two processors by using `mpirun`, as: +# Note that in addition to give how many elements we would like to have in each direction. +# We also have to supply the _MPI-communicator_. +# This is to specify how we would like the program to behave in parallel. +# If we supply `MPI.COMM_WORLD` we create a single mesh, whose data is distributed over the number of processors we +# would like to use. We can for instance run the program in parallel on two processors by using `mpirun`, as: # ``` bash # mpirun -n 2 python3 t1.py # ``` @@ -86,42 +88,45 @@ # Once the mesh has been created, we can create the finite element function space $V$. # We import the function space initializer from the `dolfinx.fem` module. -# + vscode={"languageId": "python"} -from dolfinx.fem import functionspace -V = functionspace(domain, ("Lagrange", 1)) - -# + vscode={"languageId": "python"} +# + from dolfinx import fem -uD = fem.Function(V) -uD.interpolate(lambda x: 1 + x[0]**2 + 2 * x[1]**2) + +V = fem.functionspace(domain, ("Lagrange", 1)) # - -# We now have the boundary data (and in this case the solution of -# the finite element problem) represented in the discrete function space. +# ## Dirichlet boundary conditions +# Next, we create a function that will hold the Dirichlet boundary data, and use interpolation to +# fill it with the appropriate data. + +uD = fem.Function(V) +uD.interpolate(lambda x: 1 + x[0] ** 2 + 2 * x[1] ** 2) + +# We now have the boundary data (and in this case the solution of the finite element problem) represented in the discrete function space. # Next we would like to apply the boundary values to all degrees of freedom that are on the boundary of the discrete domain. We start by identifying the facets (line-segments) representing the outer boundary, using `dolfinx.mesh.exterior_facet_indices`. -# + vscode={"languageId": "python"} -import numpy +# + [markdown] magic_args="vscode={\"languageId\": \"python\"}" +# +# - + # Create facet to cell connectivity required to determine boundary facets + tdim = domain.topology.dim fdim = tdim - 1 domain.topology.create_connectivity(fdim, tdim) boundary_facets = mesh.exterior_facet_indices(domain.topology) -# - -# For the current problem, as we are using the "Lagrange" 1 function space, the degrees of freedom are located at the vertices of each cell, thus each facet contains two degrees of freedom. +# For the current problem, as we are using the "Lagrange" 1 function space, the degrees of freedom are located at the vertices of each cell, thus each facet contains two degrees of freedom. # -# To find the local indices of these degrees of freedom, we use `dolfinx.fem.locate_dofs_topological`, which takes in the function space, the dimension of entities in the mesh we would like to identify and the local entities. +# To find the local indices of these degrees of freedom, we use `dolfinx.fem.locate_dofs_topological`, which takes in the function space, the dimension of entities in the mesh we would like to identify and the local entities. # ```{admonition} Local ordering of degrees of freedom and mesh vertices -# Many people expect there to be a 1-1 correspondence between the mesh coordinates and the coordinates of the degrees of freedom. +# Many people expect there to be a 1-1 correspondence between the mesh coordinates and the coordinates of the degrees of freedom. # However, this is only true in the case of `Lagrange` 1 elements on a first order mesh. Therefore, in DOLFINx we use separate local numbering for the mesh coordinates and the dof coordinates. To obtain the local dof coordinates we can use `V.tabulate_dof_coordinates()`, while the ordering of the local vertices can be obtained by `mesh.geometry.x`. # ``` # With this data at hand, we can create the Dirichlet boundary condition -# + vscode={"languageId": "python"} + boundary_dofs = fem.locate_dofs_topological(V, fdim, boundary_facets) bc = fem.dirichletbc(uD, boundary_dofs) -# - # ## Defining the trial and test function # @@ -130,40 +135,46 @@ # # We use the [Unified Form Language](https://github.com/FEniCS/ufl/) (UFL) to specify the varitional formulations. See {cite}`ufl2014` for more details. -# + vscode={"languageId": "python"} + +# + import ufl + u = ufl.TrialFunction(V) v = ufl.TestFunction(V) # - - # ## Defining the source term -# As the source term is constant over the domain, we use `dolfinx.Constant` +# As the source term is constant over the domain, we use `dolfinx.fem.Constant` -# + vscode={"languageId": "python"} +# + from dolfinx import default_scalar_type + f = fem.Constant(domain, default_scalar_type(-6)) # - # ```{admonition} Compilation speed-up -# Instead of wrapping $-6$ in a `dolfinx.Constant`, we could simply define $f$ as `f=-6`. -# However, if we would like to change this parameter later in the simulation, we would have to redefine our variational formulation. The `dolfinx.Constant` allows us to update the value in $f$ by using `f.value=5`. Additionally, by indicating that $f$ is a constant, we speed of compilation of the variational formulations required for the created linear system. +# Instead of wrapping $-6$ in a `dolfinx.fem.Constant`, we could simply define $f$ as `f=-6`. +# However, if we would like to change this parameter later in the simulation, we would have to redefine our variational formulation. +# The `dolfinx.fem.Constant` allows us to update the value in $f$ by using `f.value=5`. +# Additionally, by indicating that $f$ is a constant, we speed of compilation of the variational formulations required for the created linear system. # ``` # ## Defining the variational problem # As we now have defined all variables used to describe our variational problem, we can create the weak formulation -# + vscode={"languageId": "python"} a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx L = f * v * ufl.dx -# - # Note that there is a very close correspondence between the Python syntax and the mathematical syntax -# $\int_{\Omega} \nabla u \cdot \nabla v ~\mathrm{d} x$ and $\int_{\Omega}fv~\mathrm{d} x$. +# $\int_{\Omega} \nabla u \cdot \nabla v ~\mathrm{d} x$ and $\int_{\Omega}fv~\mathrm{d} x$. # The integration over the domain $\Omega$ is defined by using `ufl.dx`, an integration measure over all cells of the mesh. # # This is the key strength of FEniCSx: the formulas in the variational formulation translate directly to very similar Python code, a feature that makes it easy to specify and solve complicated PDE problems. # # ## Expressing inner products -# The inner product $\int_\Omega \nabla u \cdot \nabla v ~\mathrm{d} x$ can be expressed in various ways in UFL. We have used the notation `ufl.dot(ufl.grad(u), ufl.grad(v))*ufl.dx`. The dot product in UFL computes the sum (contraction) over the last index of the first factor and first index of the second factor. In this case, both factors are tensors of rank one (vectors) and so the sum is just over the single index of both $\nabla u$ and $\nabla v$. To compute an inner product of matrices (with two indices), one must instead of `ufl.dot` use the function `ufl.inner`. For vectors, `ufl.dot` and `ufl.inner` are equivalent. +# The inner product $\int_\Omega \nabla u \cdot \nabla v ~\mathrm{d} x$ can be expressed in various ways in UFL. We have used the notation `ufl.dot(ufl.grad(u), ufl.grad(v))*ufl.dx`. +# The dot product in UFL computes the sum (contraction) over the last index of the first factor and first index of the second factor. +# In this case, both factors are tensors of rank one (vectors) and so the sum is just over the single index of both $\nabla u$ and $\nabla v$. +# To compute an inner product of matrices (with two indices), on must use use the function `ufl.inner` instead of `ufl.dot`. +# For vectors, `ufl.dot` and `ufl.inner` are equivalent. # # ```{admonition} Complex numbers # In DOLFINx, one can solve complex number problems by using an installation of PETSc using complex numbers. @@ -175,50 +186,53 @@ # # ## Forming and solving the linear system # -# Having defined the finite element variational problem and boundary condition, we can create our `dolfinx.fem.petsc.LinearProblem`, as class for solving +# Having defined the finite element variational problem and boundary condition, we can create our `dolfinx.fem.petsc.LinearProblem`, as class for solving # the variational problem: Find $u_h\in V$ such that $a(u_h, v)==L(v) \quad \forall v \in \hat{V}$. We will use PETSc as our linear algebra backend, using a direct solver (LU-factorization). # See the [PETSc-documentation](https://petsc.org/main/docs/manual/ksp/?highlight=ksp#ksp-linear-system-solvers) of the method for more information. # PETSc is not a required dependency of DOLFINx, and therefore we explicitly import the DOLFINx wrapper for interfacing with PETSc. +# To ensure that the options passed to the LinearProblem is only used for the given KSP solver, we pass a **unique** option prefix as well. -# + vscode={"languageId": "python"} from dolfinx.fem.petsc import LinearProblem -problem = LinearProblem(a, L, bcs=[bc], petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) + +problem = LinearProblem( + a, + L, + bcs=[bc], + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="Poisson", +) uh = problem.solve() -# - # Using `problem.solve()` we solve the linear system of equations and return a `dolfinx.fem.Function` containing the solution. # ## Computing the error -# Finally, we want to compute the error to check the accuracy of the solution. We do this by comparing the finite element solution `u` with the exact solution. We do this by interpolating the exact solution into the the $P_2$-function space. +# Finally, we want to compute the error to check the accuracy of the solution. We do this by comparing the finite element solution `u` with the exact solution. +# We do this by interpolating the exact solution into the the $P_2$-function space. -# + vscode={"languageId": "python"} V2 = fem.functionspace(domain, ("Lagrange", 2)) uex = fem.Function(V2) -uex.interpolate(lambda x: 1 + x[0]**2 + 2 * x[1]**2) -# - +uex.interpolate(lambda x: 1 + x[0] ** 2 + 2 * x[1] ** 2) -# We compute the error in two different ways. First, we compute the $L^2$-norm of the error, defined by $E=\sqrt{\int_\Omega (u_D-u_h)^2\mathrm{d} x}$. We use UFL to express the $L^2$-error, and use `dolfinx.fem.assemble_scalar` to compute the scalar value. In DOLFINx, `assemble_scalar` only assembles over the cells on the local process. This means that if we use 2 processes to solve our problem, we need to gather the solution to one (or all the processes. +# We compute the error in two different ways. First, we compute the $L^2$-norm of the error, defined by $E=\sqrt{\int_\Omega (u_D-u_h)^2\mathrm{d} x}$. +# We use UFL to express the $L^2$-error, and use `dolfinx.fem.assemble_scalar` to compute the scalar value. +# In DOLFINx, `assemble_scalar` only assembles over the cells on the local process. This means that if we use 2 processes to solve our problem, we need to gather the solution to one (or all the processes. # We can do this with the `MPI.allreduce` function. -# + vscode={"languageId": "python"} L2_error = fem.form(ufl.inner(uh - uex, uh - uex) * ufl.dx) error_local = fem.assemble_scalar(L2_error) error_L2 = numpy.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM)) -# - # Secondly, we compute the maximum error at any degree of freedom. # As the finite element function $u$ can be expressed as a linear combination of basis functions $\phi_j$, spanning the space $V$: # $ u = \sum_{j=1}^N U_j\phi_j.$ # By writing `problem.solve()` we compute all the coefficients $U_1,\dots, U_N$. These values are known as the _degrees of freedom_ (dofs). We can access the degrees of freedom by accessing the underlying vector in `uh`. # However, as a second order function space has more dofs than a linear function space, we cannot compare these arrays directly. -# As we allready have interpolated the exact solution into the first order space when creating the boundary condition, we can compare the maximum values at any degree of freedom of the approximation space. +# As we already have interpolated the exact solution into the first order space when creating the boundary condition, we can compare the maximum values at any degree of freedom of the approximation space. -# + vscode={"languageId": "python"} -error_max = numpy.max(numpy.abs(uD.x.array-uh.x.array)) +error_max = numpy.max(numpy.abs(uD.x.array - uh.x.array)) # Only print the error on one process if domain.comm.rank == 0: print(f"Error_L2 : {error_L2:.2e}") print(f"Error_max : {error_max:.2e}") -# - # ## Plotting the mesh using pyvista # We will visualizing the mesh using [pyvista](https://docs.pyvista.org/), an interface to the VTK toolkit. @@ -226,23 +240,27 @@ # To do this we use the function `dolfinx.plot.vtk_mesh`. The first step is to create an unstructured grid that can be used by `pyvista`. # We need to start a virtual framebuffer for plotting through docker containers. You can print the current backend and change it with `pyvista.set_jupyter_backend(backend)` +# + import pyvista + print(pyvista.global_theme.jupyter_backend) +pyvista.start_xvfb(0.1) -# + vscode={"languageId": "python"} +# + from dolfinx import plot -pyvista.start_xvfb() + domain.topology.create_connectivity(tdim, tdim) topology, cell_types, geometry = plot.vtk_mesh(domain, tdim) grid = pyvista.UnstructuredGrid(topology, cell_types, geometry) # - -# There are several backends that can be used with pyvista, and they have different benefits and drawbacks. See the [pyvista documentation](https://docs.pyvista.org/user-guide/jupyter/index.html#state-of-3d-interactive-jupyterlab-plotting) for more information and installation details. In this example and the rest of the tutorial we will use [panel](https://github.com/holoviz/panel). +# There are several backends that can be used with pyvista, and they have different benefits and drawbacks. +# See the [pyvista documentation](https://docs.pyvista.org/user-guide/jupyter/index.html#state-of-3d-interactive-jupyterlab-plotting) for more information and installation details. +# n this example and the rest of the tutorial we will use [panel](https://github.com/holoviz/panel). # We can now use the `pyvista.Plotter` to visualize the mesh. We visualize it by showing it in 2D and warped in 3D. # In the jupyter notebook environment, we use the default setting of `pyvista.OFF_SCREEN=False`, which will render plots directly in the notebook. -# + vscode={"languageId": "python"} plotter = pyvista.Plotter() plotter.add_mesh(grid, show_edges=True) plotter.view_xy() @@ -250,18 +268,14 @@ plotter.show() else: figure = plotter.screenshot("fundamentals_mesh.png") -# - # ## Plotting a function using pyvista # We want to plot the solution `uh`. As the function space used to defined the mesh is disconnected from the function space defining the mesh, we create a mesh based on the dof coordinates for the function space `V`. We use `dolfinx.plot.vtk_mesh` with the function space as input to create a mesh with mesh geometry based on the dof coordinates. -# + vscode={"languageId": "python"} u_topology, u_cell_types, u_geometry = plot.vtk_mesh(V) -# - # Next, we create the `pyvista.UnstructuredGrid` and add the dof-values to the mesh. -# + vscode={"languageId": "python"} u_grid = pyvista.UnstructuredGrid(u_topology, u_cell_types, u_geometry) u_grid.point_data["u"] = uh.x.array.real u_grid.set_active_scalars("u") @@ -270,24 +284,22 @@ u_plotter.view_xy() if not pyvista.OFF_SCREEN: u_plotter.show() -# - # We can also warp the mesh by scalar to make use of the 3D plotting. -# + vscode={"languageId": "python"} warped = u_grid.warp_by_scalar() plotter2 = pyvista.Plotter() plotter2.add_mesh(warped, show_edges=True, show_scalar_bar=True) if not pyvista.OFF_SCREEN: plotter2.show() -# - # ## External post-processing # For post-processing outside the python code, it is suggested to save the solution to file using either `dolfinx.io.VTXWriter` or `dolfinx.io.XDMFFile` and using [Paraview](https://www.paraview.org/). This is especially suggested for 3D visualization. -# + vscode={"languageId": "python"} +# + from dolfinx import io from pathlib import Path + results_folder = Path("results") results_folder.mkdir(exist_ok=True, parents=True) filename = results_folder / "fundamentals" diff --git a/chapter1/membrane_code.ipynb b/chapter1/membrane_code.ipynb index ecff9393..1cb38da1 100644 --- a/chapter1/membrane_code.ipynb +++ b/chapter1/membrane_code.ipynb @@ -14,7 +14,7 @@ "- Create constant boundary conditions using a geometrical identifier\n", "- Use `ufl.SpatialCoordinate` to create a spatially varying function\n", "- Interpolate a `ufl.Expression` into an appropriate function space\n", - "- Evaluate a `dolfinx.Function` at any point $x$\n", + "- Evaluate a `dolfinx.fem.Function` at any point $x$\n", "- Use Paraview to visualize the solution of a PDE\n", "\n", "## Creating the mesh\n", @@ -29,16 +29,8 @@ "metadata": {}, "outputs": [], "source": [ - "import gmsh" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2", - "metadata": {}, - "outputs": [], - "source": [ + "import gmsh\n", + "\n", "gmsh.initialize()" ] }, @@ -116,11 +108,14 @@ "```{note}\n", "If you do not use `gmsh.model.addPhysicalGroup` when creating the mesh with GMSH, it can not be read into DOLFINx.\n", "```\n", - "The `MeshData` object can also contain tags for all other `PhysicalGroups` that has been added to the mesh, that being `vertex_tags`, `edge_tags`, `facet_tags` and `cell_tags`.\n", + "The `MeshData` object can also contain tags for all other `PhysicalGroups` that has been added to the mesh,\n", + "that being `vertex_tags`, `edge_tags`, `facet_tags` and `cell_tags`.\n", "To read either `gmsh.model` or a `.msh`-file, one has to distribute the mesh to all processes used by DOLFINx.\n", "As GMSH does not support mesh creation with MPI, we currently have a `gmsh.model.mesh` on each process.\n", - "To distribute the mesh, we have to specify which process the mesh was created on, and which communicator rank should distribute the mesh.\n", - "The `model_to_mesh` will then load the mesh on the specified rank, and distribute it to the communicator using a mesh partitioner." + "To distribute the mesh, we have to specify which process the mesh was created on,\n", + "and which communicator rank should distribute the mesh.\n", + "The `model_to_mesh` will then load the mesh on the specified rank,\n", + "and distribute it to the communicator using a mesh partitioner." ] }, { @@ -157,16 +152,8 @@ "metadata": {}, "outputs": [], "source": [ - "from dolfinx import fem" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "13", - "metadata": {}, - "outputs": [], - "source": [ + "from dolfinx import fem\n", + "\n", "V = fem.functionspace(domain, (\"Lagrange\", 1))" ] }, @@ -268,7 +255,11 @@ "a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx\n", "L = p * v * ufl.dx\n", "problem = LinearProblem(\n", - " a, L, bcs=[bc], petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"}\n", + " a,\n", + " L,\n", + " bcs=[bc],\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"membrane_\",\n", ")\n", "uh = problem.solve()" ] @@ -308,20 +299,50 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "03cd141d", "metadata": {}, "outputs": [], "source": [ "from dolfinx.plot import vtk_mesh\n", "import pyvista\n", "\n", - "pyvista.start_xvfb()\n", - "\n", - "# Extract topology from mesh and create pyvista mesh\n", + "pyvista.start_xvfb(0.1)" + ] + }, + { + "cell_type": "markdown", + "id": "28f12478", + "metadata": {}, + "source": [ + "Extract topology from mesh and create pyvista mesh" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c613c53", + "metadata": {}, + "outputs": [], + "source": [ "topology, cell_types, x = vtk_mesh(V)\n", - "grid = pyvista.UnstructuredGrid(topology, cell_types, x)\n", - "\n", - "# Set deflection values and add it to plotter\n", + "grid = pyvista.UnstructuredGrid(topology, cell_types, x)" + ] + }, + { + "cell_type": "markdown", + "id": "94cd00a8", + "metadata": {}, + "source": [ + "Set deflection values and add it to plotter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ "grid.point_data[\"u\"] = uh.x.array\n", "warped = grid.warp_by_scalar(\"u\", factor=25)\n", "\n", @@ -403,16 +424,8 @@ "metadata": {}, "outputs": [], "source": [ - "from dolfinx import geometry" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33", - "metadata": {}, - "outputs": [], - "source": [ + "from dolfinx import geometry\n", + "\n", "bb_tree = geometry.bb_tree(domain, domain.topology.dim)" ] }, @@ -483,16 +496,8 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "40", - "metadata": {}, - "outputs": [], - "source": [ + "import matplotlib.pyplot as plt\n", + "\n", "fig = plt.figure()\n", "plt.plot(\n", " points_on_proc[:, 1],\n", @@ -504,38 +509,46 @@ "plt.plot(points_on_proc[:, 1], p_values, \"b--\", linewidth=2, label=\"Load\")\n", "plt.grid(True)\n", "plt.xlabel(\"y\")\n", - "plt.legend()\n", - "# If run in parallel as a python file, we save a plot per processor\n", - "plt.savefig(f\"membrane_rank{MPI.COMM_WORLD.rank:d}.png\")" + "plt.legend()" ] }, { "cell_type": "markdown", - "id": "41", + "id": "d60d5e5f", "metadata": {}, "source": [ - "## Saving functions to file\n", - "As mentioned in the previous section, we can also use Paraview to visualize the solution." + "If executed in parallel as a python file, we save a plot per processor" ] }, { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "40", "metadata": {}, "outputs": [], "source": [ - "import dolfinx.io\n", - "from pathlib import Path" + "plt.savefig(f\"membrane_rank{MPI.COMM_WORLD.rank:d}.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "## Saving functions to file\n", + "As mentioned in the previous section, we can also use Paraview to visualize the solution." ] }, { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "42", "metadata": {}, "outputs": [], "source": [ + "import dolfinx.io\n", + "from pathlib import Path\n", + "\n", "pressure.name = \"Load\"\n", "uh.name = \"Deflection\"\n", "results_folder = Path(\"results\")\n", diff --git a/chapter1/membrane_code.py b/chapter1/membrane_code.py index 57b03437..4ede7110 100644 --- a/chapter1/membrane_code.py +++ b/chapter1/membrane_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -22,16 +22,18 @@ # - Create constant boundary conditions using a geometrical identifier # - Use `ufl.SpatialCoordinate` to create a spatially varying function # - Interpolate a `ufl.Expression` into an appropriate function space -# - Evaluate a `dolfinx.Function` at any point $x$ +# - Evaluate a `dolfinx.fem.Function` at any point $x$ # - Use Paraview to visualize the solution of a PDE # # ## Creating the mesh # # To create the computational geometry, we use the Python-API of [GMSH](https://gmsh.info/). We start by importing the gmsh-module and initializing it. +# + import gmsh gmsh.initialize() +# - # The next step is to create the membrane and start the computations by the GMSH CAD kernel, to generate the relevant underlying data structures. The first arguments of `addDisk` are the x, y and z coordinate of the center of the circle, while the two last arguments are the x-radius and y-radius. @@ -60,11 +62,14 @@ # ```{note} # If you do not use `gmsh.model.addPhysicalGroup` when creating the mesh with GMSH, it can not be read into DOLFINx. # ``` -# The `MeshData` object can also contain tags for all other `PhysicalGroups` that has been added to the mesh, that being `vertex_tags`, `edge_tags`, `facet_tags` and `cell_tags`. +# The `MeshData` object can also contain tags for all other `PhysicalGroups` that has been added to the mesh, +# that being `vertex_tags`, `edge_tags`, `facet_tags` and `cell_tags`. # To read either `gmsh.model` or a `.msh`-file, one has to distribute the mesh to all processes used by DOLFINx. # As GMSH does not support mesh creation with MPI, we currently have a `gmsh.model.mesh` on each process. -# To distribute the mesh, we have to specify which process the mesh was created on, and which communicator rank should distribute the mesh. -# The `model_to_mesh` will then load the mesh on the specified rank, and distribute it to the communicator using a mesh partitioner. +# To distribute the mesh, we have to specify which process the mesh was created on, +# and which communicator rank should distribute the mesh. +# The `model_to_mesh` will then load the mesh on the specified rank, +# and distribute it to the communicator using a mesh partitioner. # + from dolfinx.io import gmshio @@ -81,9 +86,11 @@ # We define the function space as in the previous tutorial +# + from dolfinx import fem V = fem.functionspace(domain, ("Lagrange", 1)) +# - # ## Defining a spatially varying load # The right hand side pressure function is represented using `ufl.SpatialCoordinate` and two constants, one for $\beta$ and one for $R_0$. @@ -122,7 +129,11 @@ def on_boundary(x): a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx L = p * v * ufl.dx problem = LinearProblem( - a, L, bcs=[bc], petsc_options={"ksp_type": "preonly", "pc_type": "lu"} + a, + L, + bcs=[bc], + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="membrane_", ) uh = problem.solve() @@ -142,13 +153,17 @@ def on_boundary(x): from dolfinx.plot import vtk_mesh import pyvista -pyvista.start_xvfb() +pyvista.start_xvfb(0.1) +# - # Extract topology from mesh and create pyvista mesh + topology, cell_types, x = vtk_mesh(V) grid = pyvista.UnstructuredGrid(topology, cell_types, x) # Set deflection values and add it to plotter + +# + grid.point_data["u"] = uh.x.array warped = grid.warp_by_scalar("u", factor=25) @@ -189,9 +204,11 @@ def on_boundary(x): # However, as a mesh consists of a large set of degrees of freedom (i.e. $N$ is large), we want to reduce the number of evaluations of the basis function $\phi_i(x)$. We do this by identifying which cell of the mesh $x$ is in. # This is efficiently done by creating a bounding box tree of the cells of the mesh, allowing a quick recursive search through the mesh entities. +# + from dolfinx import geometry bb_tree = geometry.bb_tree(domain, domain.topology.dim) +# - # Now we can compute which cells the bounding box tree collides with using `dolfinx.geometry.compute_collisions_points`. This function returns a list of cells whose bounding box collide for each input point. As different points might have different number of cells, the data is stored in `dolfinx.cpp.graph.AdjacencyList_int32`, where one can access the cells for the `i`th point by calling `links(i)`. # However, as the bounding box of a cell spans more of $\mathbb{R}^n$ than the actual cell, we check that the actual cell collides with the input point @@ -220,6 +237,7 @@ def on_boundary(x): # As we now have an array of coordinates and two arrays of function values, we can use `matplotlib` to plot them +# + import matplotlib.pyplot as plt fig = plt.figure() @@ -234,12 +252,16 @@ def on_boundary(x): plt.grid(True) plt.xlabel("y") plt.legend() -# If run in parallel as a python file, we save a plot per processor +# - + +# If executed in parallel as a python file, we save a plot per processor + plt.savefig(f"membrane_rank{MPI.COMM_WORLD.rank:d}.png") # ## Saving functions to file # As mentioned in the previous section, we can also use Paraview to visualize the solution. +# + import dolfinx.io from pathlib import Path diff --git a/chapter1/nitsche.ipynb b/chapter1/nitsche.ipynb index bc0fa6ec..5ae919ae 100644 --- a/chapter1/nitsche.ipynb +++ b/chapter1/nitsche.ipynb @@ -25,8 +25,18 @@ "from dolfinx.fem.petsc import LinearProblem\n", "import numpy\n", "from mpi4py import MPI\n", - "from ufl import (Circumradius, FacetNormal, SpatialCoordinate, TrialFunction, TestFunction,\n", - " div, dx, ds, grad, inner)\n", + "from ufl import (\n", + " Circumradius,\n", + " FacetNormal,\n", + " SpatialCoordinate,\n", + " TrialFunction,\n", + " TestFunction,\n", + " div,\n", + " dx,\n", + " ds,\n", + " grad,\n", + " inner,\n", + ")\n", "\n", "N = 8\n", "domain = mesh.create_unit_square(MPI.COMM_WORLD, N, N)\n", @@ -50,7 +60,7 @@ "source": [ "uD = fem.Function(V)\n", "x = SpatialCoordinate(domain)\n", - "u_ex = 1 + x[0]**2 + 2 * x[1]**2\n", + "u_ex = 1 + x[0] ** 2 + 2 * x[1] ** 2\n", "uD.interpolate(fem.Expression(u_ex, V.element.interpolation_points))\n", "f = -div(grad(u_ex))" ] @@ -92,9 +102,9 @@ "h = 2 * Circumradius(domain)\n", "alpha = fem.Constant(domain, default_scalar_type(10))\n", "a = inner(grad(u), grad(v)) * dx - inner(n, grad(u)) * v * ds\n", - "a += - inner(n, grad(v)) * u * ds + alpha / h * inner(u, v) * ds\n", - "L = inner(f, v) * dx \n", - "L += - inner(n, grad(v)) * uD * ds + alpha / h * inner(uD, v) * ds" + "a += -inner(n, grad(v)) * u * ds + alpha / h * inner(u, v) * ds\n", + "L = inner(f, v) * dx\n", + "L += -inner(n, grad(v)) * uD * ds + alpha / h * inner(uD, v) * ds" ] }, { @@ -112,7 +122,7 @@ "metadata": {}, "outputs": [], "source": [ - "problem = LinearProblem(a, L)\n", + "problem = LinearProblem(a, L, petsc_options_prefix=\"nitsche_poisson\")\n", "uh = problem.solve()" ] }, @@ -131,11 +141,11 @@ "metadata": {}, "outputs": [], "source": [ - "error_form = fem.form(inner(uh-uD, uh-uD) * dx)\n", + "error_form = fem.form(inner(uh - uD, uh - uD) * dx)\n", "error_local = fem.assemble_scalar(error_form)\n", "errorL2 = numpy.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))\n", "if domain.comm.rank == 0:\n", - " print(fr\"$L^2$-error: {errorL2:.2e}\")" + " print(rf\"$L^2$-error: {errorL2:.2e}\")" ] }, { @@ -154,7 +164,9 @@ "metadata": {}, "outputs": [], "source": [ - "error_max = domain.comm.allreduce(numpy.max(numpy.abs(uD.x.array-uh.x.array)), op=MPI.MAX)\n", + "error_max = domain.comm.allreduce(\n", + " numpy.max(numpy.abs(uD.x.array - uh.x.array)), op=MPI.MAX\n", + ")\n", "if domain.comm.rank == 0:\n", " print(f\"Error_max : {error_max:.2e}\")" ] @@ -175,7 +187,8 @@ "outputs": [], "source": [ "import pyvista\n", - "pyvista.start_xvfb()\n", + "\n", + "pyvista.start_xvfb(1.0)\n", "\n", "grid = pyvista.UnstructuredGrid(*plot.vtk_mesh(V))\n", "grid.point_data[\"u\"] = uh.x.array.real\n", @@ -198,14 +211,6 @@ " :filter: cited and ({\"chapter1/nitsche\"} >= docnames)\n", "```" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/chapter1/nitsche.py b/chapter1/nitsche.py index 03818d3d..0c0be728 100644 --- a/chapter1/nitsche.py +++ b/chapter1/nitsche.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -26,8 +26,18 @@ from dolfinx.fem.petsc import LinearProblem import numpy from mpi4py import MPI -from ufl import (Circumradius, FacetNormal, SpatialCoordinate, TrialFunction, TestFunction, - div, dx, ds, grad, inner) +from ufl import ( + Circumradius, + FacetNormal, + SpatialCoordinate, + TrialFunction, + TestFunction, + div, + dx, + ds, + grad, + inner, +) N = 8 domain = mesh.create_unit_square(MPI.COMM_WORLD, N, N) @@ -38,7 +48,7 @@ uD = fem.Function(V) x = SpatialCoordinate(domain) -u_ex = 1 + x[0]**2 + 2 * x[1]**2 +u_ex = 1 + x[0] ** 2 + 2 * x[1] ** 2 uD.interpolate(fem.Expression(u_ex, V.element.interpolation_points)) f = -div(grad(u_ex)) @@ -66,27 +76,29 @@ h = 2 * Circumradius(domain) alpha = fem.Constant(domain, default_scalar_type(10)) a = inner(grad(u), grad(v)) * dx - inner(n, grad(u)) * v * ds -a += - inner(n, grad(v)) * u * ds + alpha / h * inner(u, v) * ds -L = inner(f, v) * dx -L += - inner(n, grad(v)) * uD * ds + alpha / h * inner(uD, v) * ds +a += -inner(n, grad(v)) * u * ds + alpha / h * inner(u, v) * ds +L = inner(f, v) * dx +L += -inner(n, grad(v)) * uD * ds + alpha / h * inner(uD, v) * ds # As we now have the variational form, we can solve the linear problem -problem = LinearProblem(a, L) +problem = LinearProblem(a, L, petsc_options_prefix="nitsche_poisson") uh = problem.solve() # We compute the error of the computation by comparing it to the analytical solution -error_form = fem.form(inner(uh-uD, uh-uD) * dx) +error_form = fem.form(inner(uh - uD, uh - uD) * dx) error_local = fem.assemble_scalar(error_form) errorL2 = numpy.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM)) if domain.comm.rank == 0: - print(fr"$L^2$-error: {errorL2:.2e}") + print(rf"$L^2$-error: {errorL2:.2e}") # We observe that the $L^2$-error is of the same magnitude as in the first tutorial. # As in the previous tutorial, we also compute the maximal error for all the degrees of freedom. -error_max = domain.comm.allreduce(numpy.max(numpy.abs(uD.x.array-uh.x.array)), op=MPI.MAX) +error_max = domain.comm.allreduce( + numpy.max(numpy.abs(uD.x.array - uh.x.array)), op=MPI.MAX +) if domain.comm.rank == 0: print(f"Error_max : {error_max:.2e}") @@ -94,7 +106,8 @@ # + import pyvista -pyvista.start_xvfb() + +pyvista.start_xvfb(1.0) grid = pyvista.UnstructuredGrid(*plot.vtk_mesh(V)) grid.point_data["u"] = uh.x.array.real @@ -111,5 +124,3 @@ # ```{bibliography} # :filter: cited and ({"chapter1/nitsche"} >= docnames) # ``` - - diff --git a/chapter2/diffusion_code.ipynb b/chapter2/diffusion_code.ipynb index 132d2413..638db8a4 100644 --- a/chapter2/diffusion_code.ipynb +++ b/chapter2/diffusion_code.ipynb @@ -20,6 +20,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d8774e0c", "metadata": {}, "outputs": [], "source": [ @@ -32,27 +33,79 @@ "from mpi4py import MPI\n", "\n", "from dolfinx import fem, mesh, io, plot\n", - "from dolfinx.fem.petsc import assemble_vector, assemble_matrix, create_vector, apply_lifting, set_bc\n", - "\n", - "# Define temporal parameters\n", - "t = 0 # Start time\n", + "from dolfinx.fem.petsc import (\n", + " assemble_vector,\n", + " assemble_matrix,\n", + " create_vector,\n", + " apply_lifting,\n", + " set_bc,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7fe3c778", + "metadata": {}, + "source": [ + "We define the time discretization parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af1729e3", + "metadata": {}, + "outputs": [], + "source": [ + "t = 0.0 # Start time\n", "T = 1.0 # Final time\n", "num_steps = 50\n", - "dt = T / num_steps # time step size\n", - "\n", - "# Define mesh\n", + "dt = T / num_steps # time step size" + ] + }, + { + "cell_type": "markdown", + "id": "6ee139c1", + "metadata": {}, + "source": [ + "Next, we define the computational domain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ "nx, ny = 50, 50\n", - "domain = mesh.create_rectangle(MPI.COMM_WORLD, [np.array([-2, -2]), np.array([2, 2])],\n", - " [nx, ny], mesh.CellType.triangle)\n", + "domain = mesh.create_rectangle(\n", + " MPI.COMM_WORLD,\n", + " [np.array([-2, -2]), np.array([2, 2])],\n", + " [nx, ny],\n", + " mesh.CellType.triangle,\n", + ")\n", "V = fem.functionspace(domain, (\"Lagrange\", 1))" ] }, { "cell_type": "markdown", + "id": "0d1a33d9", "metadata": {}, + "source": [ + "-" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "Note that we have used a much higher resolution than before to better resolve features of the solution.\n", - "We also easily update the intial and boundary conditions. Instead of using a class to define the initial condition, we simply use a function" + "We also easily update the intial and boundary conditions.\n", + "Instead of using a class to define the initial condition, we simply use a function" ] }, { @@ -61,9 +114,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Create initial condition\n", "def initial_condition(x, a=5):\n", - " return np.exp(-a * (x[0]**2 + x[1]**2))\n", + " return np.exp(-a * (x[0] ** 2 + x[1] ** 2))\n", "\n", "\n", "u_n = fem.Function(V)\n", @@ -73,8 +125,11 @@ "# Create boundary condition\n", "fdim = domain.topology.dim - 1\n", "boundary_facets = mesh.locate_entities_boundary(\n", - " domain, fdim, lambda x: np.full(x.shape[1], True, dtype=bool))\n", - "bc = fem.dirichletbc(PETSc.ScalarType(0), fem.locate_dofs_topological(V, fdim, boundary_facets), V)" + " domain, fdim, lambda x: np.full(x.shape[1], True, dtype=bool)\n", + ")\n", + "bc = fem.dirichletbc(\n", + " PETSc.ScalarType(0), fem.locate_dofs_topological(V, fdim, boundary_facets), V\n", + ")" ] }, { @@ -90,13 +145,28 @@ { "cell_type": "code", "execution_count": null, + "id": "edfc3c1e", "metadata": {}, "outputs": [], "source": [ "xdmf = io.XDMFFile(domain.comm, \"diffusion.xdmf\", \"w\")\n", - "xdmf.write_mesh(domain)\n", - "\n", - "# Define solution variable, and interpolate initial solution for visualization in Paraview\n", + "xdmf.write_mesh(domain)" + ] + }, + { + "cell_type": "markdown", + "id": "20bd9b2c", + "metadata": {}, + "source": [ + "Define solution variable, and interpolate initial solution for visualization in Paraview" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "uh = fem.Function(V)\n", "uh.name = \"uh\"\n", "uh.interpolate(initial_condition)\n", @@ -193,7 +263,7 @@ "metadata": {}, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "\n", "grid = pyvista.UnstructuredGrid(*plot.vtk_mesh(V))\n", "\n", @@ -204,12 +274,25 @@ "warped = grid.warp_by_scalar(\"uh\", factor=1)\n", "\n", "viridis = mpl.colormaps.get_cmap(\"viridis\").resampled(25)\n", - "sargs = dict(title_font_size=25, label_font_size=20, fmt=\"%.2e\", color=\"black\",\n", - " position_x=0.1, position_y=0.8, width=0.8, height=0.1)\n", + "sargs = dict(\n", + " title_font_size=25,\n", + " label_font_size=20,\n", + " fmt=\"%.2e\",\n", + " color=\"black\",\n", + " position_x=0.1,\n", + " position_y=0.8,\n", + " width=0.8,\n", + " height=0.1,\n", + ")\n", "\n", - "renderer = plotter.add_mesh(warped, show_edges=True, lighting=False,\n", - " cmap=viridis, scalar_bar_args=sargs,\n", - " clim=[0, max(uh.x.array)])" + "renderer = plotter.add_mesh(\n", + " warped,\n", + " show_edges=True,\n", + " lighting=False,\n", + " cmap=viridis,\n", + " scalar_bar_args=sargs,\n", + " clim=[0, max(uh.x.array)],\n", + ")" ] }, { @@ -264,6 +347,26 @@ "xdmf.close()" ] }, + { + "cell_type": "markdown", + "id": "540b03b7", + "metadata": {}, + "source": [ + "We destroy the PETSc objects to avoid memory leaks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82cef59a", + "metadata": {}, + "outputs": [], + "source": [ + "A.destroy()\n", + "b.destroy()\n", + "solver.destroy()" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/chapter2/diffusion_code.py b/chapter2/diffusion_code.py index ff693ddf..e30f36d2 100644 --- a/chapter2/diffusion_code.py +++ b/chapter2/diffusion_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -35,30 +35,44 @@ from mpi4py import MPI from dolfinx import fem, mesh, io, plot -from dolfinx.fem.petsc import assemble_vector, assemble_matrix, create_vector, apply_lifting, set_bc +from dolfinx.fem.petsc import ( + assemble_vector, + assemble_matrix, + create_vector, + apply_lifting, + set_bc, +) +# - + +# We define the time discretization parameters -# Define temporal parameters -t = 0 # Start time +t = 0.0 # Start time T = 1.0 # Final time num_steps = 50 dt = T / num_steps # time step size -# Define mesh +# Next, we define the computational domain + nx, ny = 50, 50 -domain = mesh.create_rectangle(MPI.COMM_WORLD, [np.array([-2, -2]), np.array([2, 2])], - [nx, ny], mesh.CellType.triangle) +domain = mesh.create_rectangle( + MPI.COMM_WORLD, + [np.array([-2, -2]), np.array([2, 2])], + [nx, ny], + mesh.CellType.triangle, +) V = fem.functionspace(domain, ("Lagrange", 1)) # - # Note that we have used a much higher resolution than before to better resolve features of the solution. -# We also easily update the intial and boundary conditions. Instead of using a class to define the initial condition, we simply use a function +# We also easily update the intial and boundary conditions. +# Instead of using a class to define the initial condition, we simply use a function + # + -# Create initial condition def initial_condition(x, a=5): - return np.exp(-a * (x[0]**2 + x[1]**2)) + return np.exp(-a * (x[0] ** 2 + x[1] ** 2)) u_n = fem.Function(V) @@ -68,8 +82,11 @@ def initial_condition(x, a=5): # Create boundary condition fdim = domain.topology.dim - 1 boundary_facets = mesh.locate_entities_boundary( - domain, fdim, lambda x: np.full(x.shape[1], True, dtype=bool)) -bc = fem.dirichletbc(PETSc.ScalarType(0), fem.locate_dofs_topological(V, fdim, boundary_facets), V) + domain, fdim, lambda x: np.full(x.shape[1], True, dtype=bool) +) +bc = fem.dirichletbc( + PETSc.ScalarType(0), fem.locate_dofs_topological(V, fdim, boundary_facets), V +) # - # ## Time-dependent output @@ -77,16 +94,15 @@ def initial_condition(x, a=5): # The first argument to the XDMFFile is which communicator should be used to store the data. As we would like one output, independent of the number of processors, we use the `COMM_WORLD`. The second argument is the file name of the output file, while the third argument is the state of the file, # this could be read (`"r"`), write (`"w"`) or append (`"a"`). -# + xdmf = io.XDMFFile(domain.comm, "diffusion.xdmf", "w") xdmf.write_mesh(domain) # Define solution variable, and interpolate initial solution for visualization in Paraview + uh = fem.Function(V) uh.name = "uh" uh.interpolate(initial_condition) xdmf.write_function(uh, t) -# - # ## Variational problem and solver # As in the previous example, we prepare objects for time dependent problems, such that we do not have to recreate data-structures. @@ -120,7 +136,7 @@ def initial_condition(x, a=5): # We use the DOLFINx plotting functionality, which is based on pyvista to plot the solution at every $15$th time step. We would also like to visualize a colorbar reflecting the minimal and maximum value of $u$ at each time step. We use the following convenience function `plot_function` for this: # + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) grid = pyvista.UnstructuredGrid(*plot.vtk_mesh(V)) @@ -131,12 +147,25 @@ def initial_condition(x, a=5): warped = grid.warp_by_scalar("uh", factor=1) viridis = mpl.colormaps.get_cmap("viridis").resampled(25) -sargs = dict(title_font_size=25, label_font_size=20, fmt="%.2e", color="black", - position_x=0.1, position_y=0.8, width=0.8, height=0.1) - -renderer = plotter.add_mesh(warped, show_edges=True, lighting=False, - cmap=viridis, scalar_bar_args=sargs, - clim=[0, max(uh.x.array)]) +sargs = dict( + title_font_size=25, + label_font_size=20, + fmt="%.2e", + color="black", + position_x=0.1, + position_y=0.8, + width=0.8, + height=0.1, +) + +renderer = plotter.add_mesh( + warped, + show_edges=True, + lighting=False, + cmap=viridis, + scalar_bar_args=sargs, + clim=[0, max(uh.x.array)], +) # - # ## Updating the solution and right hand side per time step @@ -177,6 +206,12 @@ def initial_condition(x, a=5): plotter.close() xdmf.close() +# We destroy the PETSc objects to avoid memory leaks. + +A.destroy() +b.destroy() +solver.destroy() + # gif # ## Animation with Paraview diff --git a/chapter2/heat_code.ipynb b/chapter2/heat_code.ipynb index a3774b01..31ca0120 100644 --- a/chapter2/heat_code.ipynb +++ b/chapter2/heat_code.ipynb @@ -29,13 +29,37 @@ "from mpi4py import MPI\n", "import ufl\n", "from dolfinx import mesh, fem\n", - "from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, create_vector, set_bc\n", + "from dolfinx.fem.petsc import (\n", + " assemble_matrix,\n", + " assemble_vector,\n", + " apply_lifting,\n", + " create_vector,\n", + " set_bc,\n", + ")\n", "import numpy\n", - "t = 0 # Start time\n", - "T = 2 # End time\n", + "import numpy.typing as npt" + ] + }, + { + "cell_type": "markdown", + "id": "9bcadb71", + "metadata": {}, + "source": [ + "We define the problem specific parameters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c9f23e5", + "metadata": {}, + "outputs": [], + "source": [ + "t = 0.0 # Start time\n", + "T = 2.0 # End time\n", "num_steps = 20 # Number of time steps\n", "dt = (T - t) / num_steps # Time step size\n", - "alpha = 3\n", + "alpha = 3.0\n", "beta = 1.2" ] }, @@ -52,7 +76,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "nx, ny = 5, 5\n", "domain = mesh.create_unit_square(MPI.COMM_WORLD, nx, ny, mesh.CellType.triangle)\n", "V = fem.functionspace(domain, (\"Lagrange\", 1))" @@ -60,7 +83,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "## Defining the exact solution\n", "As in the membrane problem, we create a Python-class to resemble the exact solution" @@ -72,23 +97,17 @@ "metadata": {}, "outputs": [], "source": [ - "class exact_solution():\n", - " def __init__(self, alpha, beta, t):\n", + "class ExactSolution:\n", + " def __init__(self, alpha: float, beta: float, t: float):\n", " self.alpha = alpha\n", " self.beta = beta\n", " self.t = t\n", "\n", - " def __call__(self, x):\n", - " return 1 + x[0]**2 + self.alpha * x[1]**2 + self.beta * self.t" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "u_exact = exact_solution(alpha, beta, t)" + " def __call__(self, x: npt.NDArray[numpy.floating]) -> npt.NDArray[numpy.floating]:\n", + " return 1 + x[0] ** 2 + self.alpha * x[1] ** 2 + self.beta * self.t\n", + "\n", + "\n", + "u_exact = ExactSolution(alpha, beta, t)" ] }, { @@ -162,7 +181,11 @@ "outputs": [], "source": [ "u, v = ufl.TrialFunction(V), ufl.TestFunction(V)\n", - "F = u * v * ufl.dx + dt * ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx - (u_n + dt * f) * v * ufl.dx\n", + "F = (\n", + " u * v * ufl.dx\n", + " + dt * ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx\n", + " - (u_n + dt * f) * v * ufl.dx\n", + ")\n", "a = fem.form(ufl.lhs(F))\n", "L = fem.form(ufl.rhs(F))" ] @@ -249,6 +272,26 @@ " u_n.x.array[:] = uh.x.array" ] }, + { + "cell_type": "markdown", + "id": "727ea5c8", + "metadata": {}, + "source": [ + "We free the PETSc object to avoid memory leaks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d996c52e", + "metadata": {}, + "outputs": [], + "source": [ + "A.destroy()\n", + "b.destroy()\n", + "solver.destroy()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -270,12 +313,18 @@ "V_ex = fem.functionspace(domain, (\"Lagrange\", 2))\n", "u_ex = fem.Function(V_ex)\n", "u_ex.interpolate(u_exact)\n", - "error_L2 = numpy.sqrt(domain.comm.allreduce(fem.assemble_scalar(fem.form((uh - u_ex)**2 * ufl.dx)), op=MPI.SUM))\n", + "error_L2 = numpy.sqrt(\n", + " domain.comm.allreduce(\n", + " fem.assemble_scalar(fem.form((uh - u_ex) ** 2 * ufl.dx)), op=MPI.SUM\n", + " )\n", + ")\n", "if domain.comm.rank == 0:\n", " print(f\"L2-error: {error_L2:.2e}\")\n", "\n", "# Compute values at mesh vertices\n", - "error_max = domain.comm.allreduce(numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX)\n", + "error_max = domain.comm.allreduce(\n", + " numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX\n", + ")\n", "if domain.comm.rank == 0:\n", " print(f\"Error_max: {error_max:.2e}\")" ] diff --git a/chapter2/heat_code.py b/chapter2/heat_code.py index 9db0d9bc..24923f1f 100644 --- a/chapter2/heat_code.py +++ b/chapter2/heat_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -31,40 +31,48 @@ from mpi4py import MPI import ufl from dolfinx import mesh, fem -from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, create_vector, set_bc +from dolfinx.fem.petsc import ( + assemble_matrix, + assemble_vector, + apply_lifting, + create_vector, + set_bc, +) import numpy -t = 0 # Start time -T = 2 # End time +import numpy.typing as npt + +# We define the problem specific parameters + +t = 0.0 # Start time +T = 2.0 # End time num_steps = 20 # Number of time steps dt = (T - t) / num_steps # Time step size -alpha = 3 +alpha = 3.0 beta = 1.2 # As for the previous problem, we define the mesh and appropriate function spaces. -# + - nx, ny = 5, 5 domain = mesh.create_unit_square(MPI.COMM_WORLD, nx, ny, mesh.CellType.triangle) V = fem.functionspace(domain, ("Lagrange", 1)) - -# - - # ## Defining the exact solution # As in the membrane problem, we create a Python-class to resemble the exact solution -class exact_solution(): - def __init__(self, alpha, beta, t): + +# + +class ExactSolution: + def __init__(self, alpha: float, beta: float, t: float): self.alpha = alpha self.beta = beta self.t = t - def __call__(self, x): - return 1 + x[0]**2 + self.alpha * x[1]**2 + self.beta * self.t + def __call__(self, x: npt.NDArray[numpy.floating]) -> npt.NDArray[numpy.floating]: + return 1 + x[0] ** 2 + self.alpha * x[1] ** 2 + self.beta * self.t -u_exact = exact_solution(alpha, beta, t) +u_exact = ExactSolution(alpha, beta, t) +# - # ## Defining the boundary condition # As in the previous chapters, we define a Dirichlet boundary condition over the whole boundary @@ -90,7 +98,11 @@ def __call__(self, x): # We can now create our variational formulation, with the bilinear form `a` and linear form `L`. u, v = ufl.TrialFunction(V), ufl.TestFunction(V) -F = u * v * ufl.dx + dt * ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx - (u_n + dt * f) * v * ufl.dx +F = ( + u * v * ufl.dx + + dt * ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx + - (u_n + dt * f) * v * ufl.dx +) a = fem.form(ufl.lhs(F)) L = fem.form(ufl.rhs(F)) @@ -140,6 +152,12 @@ def __call__(self, x): # Update solution at previous time step (u_n) u_n.x.array[:] = uh.x.array +# We free the PETSc object to avoid memory leaks. + +A.destroy() +b.destroy() +solver.destroy() + # ## Verifying the numerical solution # As in the first chapter, we compute the L2-error and the error at the mesh vertices for the last time step. # to verify our implementation. @@ -149,11 +167,17 @@ def __call__(self, x): V_ex = fem.functionspace(domain, ("Lagrange", 2)) u_ex = fem.Function(V_ex) u_ex.interpolate(u_exact) -error_L2 = numpy.sqrt(domain.comm.allreduce(fem.assemble_scalar(fem.form((uh - u_ex)**2 * ufl.dx)), op=MPI.SUM)) +error_L2 = numpy.sqrt( + domain.comm.allreduce( + fem.assemble_scalar(fem.form((uh - u_ex) ** 2 * ufl.dx)), op=MPI.SUM + ) +) if domain.comm.rank == 0: print(f"L2-error: {error_L2:.2e}") # Compute values at mesh vertices -error_max = domain.comm.allreduce(numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX) +error_max = domain.comm.allreduce( + numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX +) if domain.comm.rank == 0: print(f"Error_max: {error_max:.2e}") diff --git a/chapter2/helmholtz_code.ipynb b/chapter2/helmholtz_code.ipynb index fd8ac14c..78cdfcd5 100644 --- a/chapter2/helmholtz_code.ipynb +++ b/chapter2/helmholtz_code.ipynb @@ -14,12 +14,18 @@ "\n", "## Test problem\n", "As an example, we will model a plane wave propagating in a tube.\n", - "While it is a basic test case, the code can be adapted to way more complex problems where velocity and impedance boundary conditions are needed.\n", - "We will apply a velocity boundary condition $v_n = 0.001$ to one end of the tube (for the sake of simplicity, in this basic example, we are ignoring the point source, which can be applied with scifem) and an impedance $Z$ computed with the Delaney-Bazley model,\n", - "supposing that a layer of thickness $d = 0.02$ and flow resistivity $\\sigma = 1e4$ is placed at the second end of the tube.\n", - "The choice of such impedance (the one of a plane wave propagating in free field) will give, as a result, a solution with no reflections.\n", - "\n", - "First, we create the mesh with gmsh, also setting the physical group for velocity and impedance boundary conditions and the respective tags." + "While it is a basic test case, the code can be adapted to way more complex problems where\n", + "velocity and impedance boundary conditions are needed.\n", + "We will apply a velocity boundary condition $v_n = 0.001$ to one end of the tube\n", + "(for the sake of simplicity, in this basic example, we are ignoring the point source, which can be applied with scifem)\n", + "and an impedance $Z$ computed with the Delaney-Bazley model,\n", + "supposing that a layer of thickness $d = 0.02$ and flow resistivity $\\sigma = 1e4$ is\n", + "placed at the second end of the tube.\n", + "The choice of such impedance (the one of a plane wave propagating in free field) will give, as a result,\n", + "a solution with no reflections.\n", + "\n", + "First, we create the mesh with gmsh, also setting the physical group for velocity and impedance boundary\n", + "conditions and the respective tags." ] }, { @@ -202,20 +208,17 @@ "The Delaney-Bazley model is used to compute the characteristic impedance and wavenumber of the porous layer,\n", "treated as an equivalent fluid with complex valued properties\n", "\n", - "$$\n", "\\begin{align}\n", "Z_c(\\omega) &= \\rho_0 c_0 \\left[1 + 0.0571 X^{-0.754} - j 0.087 X^{-0.732}\\right],\\\\\n", "k_c(\\omega) &= \\frac{\\omega}{c_0} \\left[1 + 0.0978 X^{-0.700} - j 0.189 X^{-0.595}\\right],\\\\\n", "\\end{align}\n", - "$$\n", "\n", "where $X = \\frac{\\rho_0 f}{\\sigma}$.\n", "\n", "With these, we can compute the surface impedance, that in the case of a rigid passive absorber placed on a rigid wall is given by the formula\n", + "\n", "$$\n", - "\\begin{align}\n", "Z_s = -j Z_c cot(k_c d).\n", - "\\end{align}\n", "$$\n", "\n", "Let's create a function to compute it.\n", @@ -246,7 +249,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Since we are going to compute a sound pressure spectrum, all the variables that depend on frequency (that are $\\omega$, $k$ and $Z$) need to be updated in the frequency loop.\n", + "Since we are going to compute a sound pressure spectrum, all the variables that depend on frequency\n", + "($\\omega$, $k$ and $Z$) need to be updated in the frequency loop.\n", "To make this possible, we will initialize them as dolfinx constants.\n", "Then, we define the value for the normal velocity on the first end of the tube" ] @@ -267,7 +271,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We also need to specify the integration measure $ds$, by using ```ufl```, and its built in integration measures" + "We also need to specify the integration measure $ds$, by using `ufl`, and its built in integration measures" ] }, { @@ -332,6 +336,7 @@ " \"pc_type\": \"lu\",\n", " \"pc_factor_mat_solver_type\": \"mumps\",\n", " },\n", + " petsc_options_prefix=\"helmholtz\",\n", ")" ] }, diff --git a/chapter2/helmholtz_code.py b/chapter2/helmholtz_code.py index e4b93ed9..adf0b172 100644 --- a/chapter2/helmholtz_code.py +++ b/chapter2/helmholtz_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.6 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (DOLFINx complex) # language: python @@ -23,12 +23,18 @@ # # ## Test problem # As an example, we will model a plane wave propagating in a tube. -# While it is a basic test case, the code can be adapted to way more complex problems where velocity and impedance boundary conditions are needed. -# We will apply a velocity boundary condition $v_n = 0.001$ to one end of the tube (for the sake of simplicity, in this basic example, we are ignoring the point source, which can be applied with scifem) and an impedance $Z$ computed with the Delaney-Bazley model, -# supposing that a layer of thickness $d = 0.02$ and flow resistivity $\sigma = 1e4$ is placed at the second end of the tube. -# The choice of such impedance (the one of a plane wave propagating in free field) will give, as a result, a solution with no reflections. +# While it is a basic test case, the code can be adapted to way more complex problems where +# velocity and impedance boundary conditions are needed. +# We will apply a velocity boundary condition $v_n = 0.001$ to one end of the tube +# (for the sake of simplicity, in this basic example, we are ignoring the point source, which can be applied with scifem) +# and an impedance $Z$ computed with the Delaney-Bazley model, +# supposing that a layer of thickness $d = 0.02$ and flow resistivity $\sigma = 1e4$ is +# placed at the second end of the tube. +# The choice of such impedance (the one of a plane wave propagating in free field) will give, as a result, +# a solution with no reflections. # -# First, we create the mesh with gmsh, also setting the physical group for velocity and impedance boundary conditions and the respective tags. +# First, we create the mesh with gmsh, also setting the physical group for velocity and impedance boundary +# conditions and the respective tags. # + import gmsh @@ -106,20 +112,17 @@ # The Delaney-Bazley model is used to compute the characteristic impedance and wavenumber of the porous layer, # treated as an equivalent fluid with complex valued properties # -# $$ # \begin{align} # Z_c(\omega) &= \rho_0 c_0 \left[1 + 0.0571 X^{-0.754} - j 0.087 X^{-0.732}\right],\\ # k_c(\omega) &= \frac{\omega}{c_0} \left[1 + 0.0978 X^{-0.700} - j 0.189 X^{-0.595}\right],\\ # \end{align} -# $$ # # where $X = \frac{\rho_0 f}{\sigma}$. # # With these, we can compute the surface impedance, that in the case of a rigid passive absorber placed on a rigid wall is given by the formula +# # $$ -# \begin{align} # Z_s = -j Z_c cot(k_c d). -# \end{align} # $$ # # Let's create a function to compute it. @@ -142,7 +145,8 @@ def delany_bazley_layer(f, rho0, c, sigma): Z_s = delany_bazley_layer(freq, rho0, c, sigma) # - -# Since we are going to compute a sound pressure spectrum, all the variables that depend on frequency (that are $\omega$, $k$ and $Z$) need to be updated in the frequency loop. +# Since we are going to compute a sound pressure spectrum, all the variables that depend on frequency +# ($\omega$, $k$ and $Z$) need to be updated in the frequency loop. # To make this possible, we will initialize them as dolfinx constants. # Then, we define the value for the normal velocity on the first end of the tube @@ -151,7 +155,7 @@ def delany_bazley_layer(f, rho0, c, sigma): Z = fem.Constant(domain, default_scalar_type(0)) v_n = 1e-5 -# We also need to specify the integration measure $ds$, by using ```ufl```, and its built in integration measures +# We also need to specify the integration measure $ds$, by using `ufl`, and its built in integration measures ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tags) @@ -186,6 +190,7 @@ def delany_bazley_layer(f, rho0, c, sigma): "pc_type": "lu", "pc_factor_mat_solver_type": "mumps", }, + petsc_options_prefix="helmholtz", ) diff --git a/chapter2/hyperelasticity.ipynb b/chapter2/hyperelasticity.ipynb index 3906a2d4..ec2d6c5e 100644 --- a/chapter2/hyperelasticity.ipynb +++ b/chapter2/hyperelasticity.ipynb @@ -21,28 +21,33 @@ "execution_count": null, "id": "1", "metadata": { + "lines_to_end_of_cell_marker": 2, "tags": [] }, "outputs": [], "source": [ "from dolfinx import log, default_scalar_type\n", "from dolfinx.fem.petsc import NonlinearProblem\n", - "from dolfinx.nls.petsc import NewtonSolver\n", "import pyvista\n", "import numpy as np\n", "import ufl\n", "\n", "from mpi4py import MPI\n", "from dolfinx import fem, mesh, plot\n", + "\n", "L = 20.0\n", - "domain = mesh.create_box(MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, 1, 1]], [20, 5, 5], mesh.CellType.hexahedron)\n", - "V = fem.functionspace(domain, (\"Lagrange\", 2, (domain.geometry.dim, )))" + "domain = mesh.create_box(\n", + " MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, 1, 1]], [20, 5, 5], mesh.CellType.hexahedron\n", + ")\n", + "V = fem.functionspace(domain, (\"Lagrange\", 2, (domain.geometry.dim,)))" ] }, { "cell_type": "markdown", "id": "2", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "We create two python functions for determining the facets to apply boundary conditions to" ] @@ -75,6 +80,14 @@ "Next, we create a marker based on these two functions" ] }, + { + "cell_type": "markdown", + "id": "4cc3b03b", + "metadata": {}, + "source": [ + "Concatenate and sort the arrays based on facet indices. Left facets marked with 1, right facets with two" + ] + }, { "cell_type": "code", "execution_count": null, @@ -82,11 +95,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Concatenate and sort the arrays based on facet indices. Left facets marked with 1, right facets with two\n", "marked_facets = np.hstack([left_facets, right_facets])\n", "marked_values = np.hstack([np.full_like(left_facets, 1), np.full_like(right_facets, 2)])\n", "sorted_facets = np.argsort(marked_facets)\n", - "facet_tag = mesh.meshtags(domain, fdim, marked_facets[sorted_facets], marked_values[sorted_facets])" + "facet_tag = mesh.meshtags(\n", + " domain, fdim, marked_facets[sorted_facets], marked_values[sorted_facets]\n", + ")" ] }, { @@ -201,25 +215,64 @@ "id": "16", "metadata": {}, "source": [ - "Define the elasticity model via a stored strain energy density function $\\psi$, and create the expression for the first Piola-Kirchhoff stress:" + "Define the elasticity model via a stored strain energy density function $\\psi$,\n", + "and create the expression for the first Piola-Kirchhoff stress:" + ] + }, + { + "cell_type": "markdown", + "id": "92d1caa5", + "metadata": {}, + "source": [ + "Elasticity parameters" ] }, { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "f7d53250", "metadata": {}, "outputs": [], "source": [ - "# Elasticity parameters\n", "E = default_scalar_type(1.0e4)\n", "nu = default_scalar_type(0.3)\n", "mu = fem.Constant(domain, E / (2 * (1 + nu)))\n", - "lmbda = fem.Constant(domain, E * nu / ((1 + nu) * (1 - 2 * nu)))\n", - "# Stored strain energy density (compressible neo-Hookean model)\n", - "psi = (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J))**2\n", - "# Stress\n", - "# Hyper-elasticity\n", + "lmbda = fem.Constant(domain, E * nu / ((1 + nu) * (1 - 2 * nu)))" + ] + }, + { + "cell_type": "markdown", + "id": "ba3318b8", + "metadata": {}, + "source": [ + "Stored strain energy density (compressible neo-Hookean model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa4714e8", + "metadata": {}, + "outputs": [], + "source": [ + "psi = (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J)) ** 2" + ] + }, + { + "cell_type": "markdown", + "id": "5e896077", + "metadata": {}, + "source": [ + "Hyper-elasticity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ "P = ufl.diff(psi, F)" ] }, @@ -229,7 +282,8 @@ "metadata": {}, "source": [ "```{admonition} Comparison to linear elasticity\n", - "To illustrate the difference between linear and hyperelasticity, the following lines can be uncommented to solve the linear elasticity problem.\n", + "To illustrate the difference between linear and hyperelasticity,\n", + "the following lines can be uncommented to solve the linear elasticity problem.\n", "```" ] }, @@ -248,62 +302,70 @@ "id": "20", "metadata": {}, "source": [ - "Define the variational form with traction integral over all facets with value 2. We set the quadrature degree for the integrals to 4." + "Define the variational form with traction integral over all facets with value 2.\n", + "We set the quadrature degree for the integrals to 4." ] }, { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "1818040a", "metadata": {}, "outputs": [], "source": [ "metadata = {\"quadrature_degree\": 4}\n", - "ds = ufl.Measure('ds', domain=domain, subdomain_data=facet_tag, metadata=metadata)\n", - "dx = ufl.Measure(\"dx\", domain=domain, metadata=metadata)\n", - "# Define form F (we want to find u such that F(u) = 0)\n", - "F = ufl.inner(ufl.grad(v), P) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2)" + "ds = ufl.Measure(\"ds\", domain=domain, subdomain_data=facet_tag, metadata=metadata)\n", + "dx = ufl.Measure(\"dx\", domain=domain, metadata=metadata)" ] }, { "cell_type": "markdown", - "id": "22", + "id": "8e85de5a", "metadata": {}, "source": [ - "As the varitional form is non-linear and written on residual form, we use the non-linear problem class from DOLFINx to set up required structures to use a Newton solver." + "Define form F (we want to find u such that F(u) = 0)" ] }, { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "21", "metadata": {}, "outputs": [], "source": [ - "problem = NonlinearProblem(F, u, bcs)" + "F = ufl.inner(ufl.grad(v), P) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2)" ] }, { "cell_type": "markdown", - "id": "24", + "id": "22", "metadata": {}, "source": [ - "and then create and customize the Newton solver" + "As the varitional form is non-linear and written on residual form,\n", + "we use the non-linear problem class from DOLFINx to set up required structures to use a Newton solver." ] }, { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "23", "metadata": {}, "outputs": [], "source": [ - "solver = NewtonSolver(domain.comm, problem)\n", - "\n", - "# Set Newton solver options\n", - "solver.atol = 1e-8\n", - "solver.rtol = 1e-8\n", - "solver.convergence_criterion = \"incremental\"\n" + "petsc_options = {\n", + " \"snes_type\": \"newtonls\",\n", + " \"snes_linesearch_type\": \"none\",\n", + " \"snes_monitor\": None,\n", + " \"snes_atol\": 1e-8,\n", + " \"snes_rtol\": 1e-8,\n", + " \"snes_stol\": 1e-8,\n", + " \"ksp_type\": \"preonly\",\n", + " \"pc_type\": \"lu\",\n", + " \"pc_factor_mat_solver_type\": \"mumps\",\n", + "}\n", + "problem = NonlinearProblem(\n", + " F, u, bcs=bcs, petsc_options=petsc_options, petsc_options_prefix=\"hyperelasticity\"\n", + ")" ] }, { @@ -321,7 +383,7 @@ "metadata": {}, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "plotter = pyvista.Plotter()\n", "plotter.open_gif(\"deformation.gif\", fps=3)\n", "\n", @@ -329,7 +391,7 @@ "function_grid = pyvista.UnstructuredGrid(topology, cells, geometry)\n", "\n", "values = np.zeros((geometry.shape[0], 3))\n", - "values[:, :len(u)] = u.x.array.reshape(geometry.shape[0], len(u))\n", + "values[:, : len(u)] = u.x.array.reshape(geometry.shape[0], len(u))\n", "function_grid[\"u\"] = values\n", "function_grid.set_active_vectors(\"u\")\n", "\n", @@ -343,7 +405,9 @@ "# Compute magnitude of displacement to visualize in GIF\n", "Vs = fem.functionspace(domain, (\"Lagrange\", 2))\n", "magnitude = fem.Function(Vs)\n", - "us = fem.Expression(ufl.sqrt(sum([u[i]**2 for i in range(len(u))])), Vs.element.interpolation_points)\n", + "us = fem.Expression(\n", + " ufl.sqrt(sum([u[i] ** 2 for i in range(len(u))])), Vs.element.interpolation_points\n", + ")\n", "magnitude.interpolate(us)\n", "warped[\"mag\"] = magnitude.x.array" ] @@ -367,11 +431,13 @@ "tval0 = -1.5\n", "for n in range(1, 10):\n", " T.value[2] = n * tval0\n", - " num_its, converged = solver.solve(u)\n", - " assert (converged)\n", - " u.x.scatter_forward()\n", + " problem.solve()\n", + " converged = problem.solver.getConvergedReason() > 0\n", + " num_its = problem.solver.getIterationNumber()\n", + " assert converged > 0, f\"Solver did not converge with reason {converged}.\"\n", + "\n", " print(f\"Time step {n}, Number of iterations {num_its}, Load {T.value}\")\n", - " function_grid[\"u\"][:, :len(u)] = u.x.array.reshape(geometry.shape[0], len(u))\n", + " function_grid[\"u\"][:, : len(u)] = u.x.array.reshape(geometry.shape[0], len(u))\n", " magnitude.interpolate(us)\n", " warped.set_active_scalars(\"mag\")\n", " warped_n = function_grid.warp_by_vector(factor=1)\n", diff --git a/chapter2/hyperelasticity.py b/chapter2/hyperelasticity.py index 0a9d4fd9..18839310 100644 --- a/chapter2/hyperelasticity.py +++ b/chapter2/hyperelasticity.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -26,22 +26,25 @@ # + from dolfinx import log, default_scalar_type from dolfinx.fem.petsc import NonlinearProblem -from dolfinx.nls.petsc import NewtonSolver import pyvista import numpy as np import ufl from mpi4py import MPI from dolfinx import fem, mesh, plot + L = 20.0 -domain = mesh.create_box(MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, 1, 1]], [20, 5, 5], mesh.CellType.hexahedron) -V = fem.functionspace(domain, ("Lagrange", 2, (domain.geometry.dim, ))) +domain = mesh.create_box( + MPI.COMM_WORLD, [[0.0, 0.0, 0.0], [L, 1, 1]], [20, 5, 5], mesh.CellType.hexahedron +) +V = fem.functionspace(domain, ("Lagrange", 2, (domain.geometry.dim,))) # - # We create two python functions for determining the facets to apply boundary conditions to + # + def left(x): return np.isclose(x[0], 0) @@ -59,10 +62,13 @@ def right(x): # Next, we create a marker based on these two functions # Concatenate and sort the arrays based on facet indices. Left facets marked with 1, right facets with two + marked_facets = np.hstack([left_facets, right_facets]) marked_values = np.hstack([np.full_like(left_facets, 1), np.full_like(right_facets, 2)]) sorted_facets = np.argsort(marked_facets) -facet_tag = mesh.meshtags(domain, fdim, marked_facets[sorted_facets], marked_values[sorted_facets]) +facet_tag = mesh.meshtags( + domain, fdim, marked_facets[sorted_facets], marked_values[sorted_facets] +) # We then create a function for supplying the boundary condition on the left side, which is fixed. @@ -103,55 +109,66 @@ def right(x): J = ufl.variable(ufl.det(F)) # - -# Define the elasticity model via a stored strain energy density function $\psi$, and create the expression for the first Piola-Kirchhoff stress: +# Define the elasticity model via a stored strain energy density function $\psi$, +# and create the expression for the first Piola-Kirchhoff stress: # Elasticity parameters + E = default_scalar_type(1.0e4) nu = default_scalar_type(0.3) mu = fem.Constant(domain, E / (2 * (1 + nu))) lmbda = fem.Constant(domain, E * nu / ((1 + nu) * (1 - 2 * nu))) + # Stored strain energy density (compressible neo-Hookean model) -psi = (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J))**2 -# Stress + +psi = (mu / 2) * (Ic - 3) - mu * ufl.ln(J) + (lmbda / 2) * (ufl.ln(J)) ** 2 + # Hyper-elasticity + P = ufl.diff(psi, F) # ```{admonition} Comparison to linear elasticity -# To illustrate the difference between linear and hyperelasticity, the following lines can be uncommented to solve the linear elasticity problem. +# To illustrate the difference between linear and hyperelasticity, +# the following lines can be uncommented to solve the linear elasticity problem. # ``` # + # P = 2.0 * mu * ufl.sym(ufl.grad(u)) + lmbda * ufl.tr(ufl.sym(ufl.grad(u))) * I # - -# Define the variational form with traction integral over all facets with value 2. We set the quadrature degree for the integrals to 4. +# Define the variational form with traction integral over all facets with value 2. +# We set the quadrature degree for the integrals to 4. metadata = {"quadrature_degree": 4} -ds = ufl.Measure('ds', domain=domain, subdomain_data=facet_tag, metadata=metadata) +ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tag, metadata=metadata) dx = ufl.Measure("dx", domain=domain, metadata=metadata) -# Define form F (we want to find u such that F(u) = 0) -F = ufl.inner(ufl.grad(v), P) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2) - -# As the varitional form is non-linear and written on residual form, we use the non-linear problem class from DOLFINx to set up required structures to use a Newton solver. - -problem = NonlinearProblem(F, u, bcs) -# and then create and customize the Newton solver - -# + -solver = NewtonSolver(domain.comm, problem) +# Define form F (we want to find u such that F(u) = 0) -# Set Newton solver options -solver.atol = 1e-8 -solver.rtol = 1e-8 -solver.convergence_criterion = "incremental" +F = ufl.inner(ufl.grad(v), P) * dx - ufl.inner(v, B) * dx - ufl.inner(v, T) * ds(2) -# - +# As the varitional form is non-linear and written on residual form, +# we use the non-linear problem class from DOLFINx to set up required structures to use a Newton solver. + +petsc_options = { + "snes_type": "newtonls", + "snes_linesearch_type": "none", + "snes_monitor": None, + "snes_atol": 1e-8, + "snes_rtol": 1e-8, + "snes_stol": 1e-8, + "ksp_type": "preonly", + "pc_type": "lu", + "pc_factor_mat_solver_type": "mumps", +} +problem = NonlinearProblem( + F, u, bcs=bcs, petsc_options=petsc_options, petsc_options_prefix="hyperelasticity" +) # We create a function to plot the solution at each time step. # + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) plotter = pyvista.Plotter() plotter.open_gif("deformation.gif", fps=3) @@ -159,7 +176,7 @@ def right(x): function_grid = pyvista.UnstructuredGrid(topology, cells, geometry) values = np.zeros((geometry.shape[0], 3)) -values[:, :len(u)] = u.x.array.reshape(geometry.shape[0], len(u)) +values[:, : len(u)] = u.x.array.reshape(geometry.shape[0], len(u)) function_grid["u"] = values function_grid.set_active_vectors("u") @@ -173,7 +190,9 @@ def right(x): # Compute magnitude of displacement to visualize in GIF Vs = fem.functionspace(domain, ("Lagrange", 2)) magnitude = fem.Function(Vs) -us = fem.Expression(ufl.sqrt(sum([u[i]**2 for i in range(len(u))])), Vs.element.interpolation_points) +us = fem.Expression( + ufl.sqrt(sum([u[i] ** 2 for i in range(len(u))])), Vs.element.interpolation_points +) magnitude.interpolate(us) warped["mag"] = magnitude.x.array # - @@ -184,11 +203,13 @@ def right(x): tval0 = -1.5 for n in range(1, 10): T.value[2] = n * tval0 - num_its, converged = solver.solve(u) - assert (converged) - u.x.scatter_forward() + problem.solve() + converged = problem.solver.getConvergedReason() > 0 + num_its = problem.solver.getIterationNumber() + assert converged > 0, f"Solver did not converge with reason {converged}." + print(f"Time step {n}, Number of iterations {num_its}, Load {T.value}") - function_grid["u"][:, :len(u)] = u.x.array.reshape(geometry.shape[0], len(u)) + function_grid["u"][:, : len(u)] = u.x.array.reshape(geometry.shape[0], len(u)) magnitude.interpolate(us) warped.set_active_scalars("mag") warped_n = function_grid.warp_by_vector(factor=1) diff --git a/chapter2/intro.md b/chapter2/intro.md index 45e7b24f..305ba5c9 100644 --- a/chapter2/intro.md +++ b/chapter2/intro.md @@ -1,4 +1,6 @@ # A Gallery of finite element solvers The goal of this chapter is to demonstrate how a range of important PDEs from science and engineering can be quickly solved with a few lines of DOLFINx code. -We will start with the heat equation, then continue with the nonlinear Poisson equation, the equations for linear elasticity, the Navier-Stokes equations, and finally look at how to solve systems of nonlinear advection-diffusion-reaction equations. These problems illustrate how to solve time-dependent problems, nonlinear problems, vector-valued problems and systems of PDEs. For each problem, we derive the variational formulation and express the problem in Python in a way that closely resembles the mathematics. +We will start with the heat equation, then continue with the nonlinear Poisson equation, the equations for linear elasticity, hyperelasticity, the Navier-Stokes equations and the Helmholtz equations. +These problems illustrate how to solve time-dependent problems, nonlinear problems, vector-valued problems and systems of PDEs. +For each problem, we derive the variational formulation and express the problem in Python in a way that closely resembles the mathematics. diff --git a/chapter2/linearelasticity_code.ipynb b/chapter2/linearelasticity_code.ipynb index 4f3926e9..5de32925 100644 --- a/chapter2/linearelasticity_code.ipynb +++ b/chapter2/linearelasticity_code.ipynb @@ -10,32 +10,36 @@ "In this tutorial, you will learn how to:\n", "- Use a vector function space\n", "- Create a constant boundary condition on a vector space\n", - "- Visualize cell wise constant functions\n", + "- Visualize cell-wise constant functions\n", "- Compute Von Mises stresses\n", "\n", "## Test problem\n", - "As a test example, we will model a clamped beam deformed under its own weigth in 3D. This can be modeled, by setting the right-hand side body force per unit volume to $f=(0,0,-\\rho g)$ with $\\rho$ the density of the beam and $g$ the acceleration of gravity. The beam is box-shaped with length $L$ and has a square cross section of width $W$. We set $u=u_D=(0,0,0)$ at the clamped end, x=0. The rest of the boundary is traction free, that is, we set $T=0$. We start by defining the physical variables used in the program." + "As a test example, we will model a clamped beam deformed under its own weight in 3D.\n", + "This can be modeled, by setting the right-hand side body force per unit volume to $f=(0,0,-\\rho g)$,\n", + "where $\\rho$ the density of the beam and $g$ the acceleration of gravity.\n", + "The beam is box-shaped with length $L$ and has a square cross section of width $W$.\n", + "We set $u=u_D=(0,0,0)$ at the clamped end, x=0. The rest of the boundary is traction free, that is, we set $T=0$.\n", + "We start by defining the physical variables used in the program." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "id": "f282d99b", + "metadata": {}, "outputs": [], "source": [ - "# Scaled variable\n", "import pyvista\n", "from dolfinx import mesh, fem, plot, io, default_scalar_type\n", "from dolfinx.fem.petsc import LinearProblem\n", "from mpi4py import MPI\n", "import ufl\n", "import numpy as np\n", - "L = 1\n", + "\n", + "L = 1.0\n", "W = 0.2\n", - "mu = 1\n", - "rho = 1\n", + "mu = 1.0\n", + "rho = 1.0\n", "delta = W / L\n", "gamma = 0.4 * delta**2\n", "beta = 1.25\n", @@ -48,29 +52,39 @@ "metadata": {}, "source": [ "We then create the mesh, which will consist of hexahedral elements, along with the function space.\n", - "As we want a vector element with three components, we add `(3, )` or `(domain.geometry.dim, )` to the element tuple to make it a triplet\n", - "However, we also could have used `basix.ufl`s functionality, creating a vector element `element = basix.ufl.element(\"Lagrange\", domain.topology.cell_name(), 1, shape=(domain.geometry.dim,))`, and initializing the function space as `V = dolfinx.fem.functionspace(domain, element)`." + "As we want a vector element with three components, we add `(3, )` or `(domain.geometry.dim, )` to the element tuple to make it a triplet.\n", + "However, we also could have used `basix.ufl`s functionality,\n", + "creating a vector element `el = basix.ufl.element(\"Lagrange\", domain.basix_cell(), 1, shape=(domain.geometry.dim,))`,\n", + "and initializing the function space as `V = dolfinx.fem.functionspace(domain, el)`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { + "lines_to_next_cell": 2, "tags": [] }, "outputs": [], "source": [ - "domain = mesh.create_box(MPI.COMM_WORLD, [np.array([0, 0, 0]), np.array([L, W, W])],\n", - " [20, 6, 6], cell_type=mesh.CellType.hexahedron)\n", - "V = fem.functionspace(domain, (\"Lagrange\", 1, (domain.geometry.dim, )))" + "domain = mesh.create_box(\n", + " MPI.COMM_WORLD,\n", + " [np.array([0, 0, 0]), np.array([L, W, W])],\n", + " [20, 6, 6],\n", + " cell_type=mesh.CellType.hexahedron,\n", + ")\n", + "V = fem.functionspace(domain, (\"Lagrange\", 1, (domain.geometry.dim,)))" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "## Boundary conditions\n", - "As we would like to clamp the boundary at $x=0$, we do this by using a marker function, which locates the facets where $x$ is close to zero by machine precision." + "As we would like to clamp the boundary at $x=0$, we do this by using a marker function,\n", + "which locates the facets where $x$ is close to zero by machine precision." ] }, { @@ -94,7 +108,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.Constant`" + "As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.fem.Constant`" ] }, { @@ -110,13 +124,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We also want to specify the integration measure $\\mathrm{d}s$, which should be the integral over the boundary of our domain. We do this by using `ufl`, and its built in integration measures" + "We also want to specify the integration measure $\\mathrm{d}s$, which should be the integral over the boundary of our domain.\n", + "We do this by using `ufl`, and its built in integration measures" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "ds = ufl.Measure(\"ds\", domain=domain)" @@ -124,7 +141,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "## Variational formulation\n", "We are now ready to create our variational formulation in close to mathematical syntax, as for the previous problems." @@ -137,7 +156,9 @@ "outputs": [], "source": [ "def epsilon(u):\n", - " return ufl.sym(ufl.grad(u)) # Equivalent to 0.5*(ufl.nabla_grad(u) + ufl.nabla_grad(u).T)\n", + " return ufl.sym(\n", + " ufl.grad(u)\n", + " ) # Equivalent to 0.5*(ufl.nabla_grad(u) + ufl.nabla_grad(u).T)\n", "\n", "\n", "def sigma(u):\n", @@ -160,8 +181,11 @@ "`div` and `grad`. This is because for scalar functions $\\nabla u$ has a clear meaning\n", "$\\nabla u = \\left(\\frac{\\partial u}{\\partial x}, \\frac{\\partial u}{\\partial y}, \\frac{\\partial u}{\\partial z} \\right)$.\n", "\n", - "However, if $u$ is vector valued, the meaning is less clear. Some sources define $\\nabla u$ as a matrix with the elements $\\frac{\\partial u_j}{\\partial x_i}$, while other sources prefer\n", - "$\\frac{\\partial u_i}{\\partial x_j}$. In DOLFINx `grad(u)` is defined as the matrix with elements $\\frac{\\partial u_i}{\\partial x_j}$. However, as it is common in continuum mechanics to use the other definition, `ufl` supplies us with `nabla_grad` for this purpose.\n", + "However, if $u$ is vector valued, the meaning is less clear.\n", + "Some sources define $\\nabla u$ as a matrix with the elements $\\frac{\\partial u_j}{\\partial x_i}$, while other sources prefer\n", + "$\\frac{\\partial u_i}{\\partial x_j}$.\n", + "In DOLFINx `grad(u)` is defined as the matrix with elements $\\frac{\\partial u_i}{\\partial x_j}$.\n", + "However, as it is common in continuum mechanics to use the other definition, `ufl` supplies us with `nabla_grad` for this purpose.\n", "```\n", "\n", "## Solve the linear variational problem\n", @@ -174,7 +198,13 @@ "metadata": {}, "outputs": [], "source": [ - "problem = LinearProblem(a, L, bcs=[bc], petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=[bc],\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"linear_elasticity\",\n", + ")\n", "uh = problem.solve()" ] }, @@ -189,7 +219,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As in the previous demos, we can either use Pyvista or Paraview for visualization. We start by using Pyvista. Instead of adding scalar values to the grid, we add vectors." + "As in the previous demos, we can either use Pyvista or Paraview for visualization.\n", + "We start by using Pyvista.\n", + "In previous tutorials, we have considered scalar values, while the following section considers vectors." ] }, { @@ -198,7 +230,7 @@ "metadata": {}, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "\n", "# Create plotter and pyvista grid\n", "p = pyvista.Plotter()\n", @@ -223,7 +255,11 @@ "source": [ "We could also use Paraview for visualizing this.\n", "As explained in previous sections, we save the solution with `XDMFFile`.\n", - "After opening the file `deformation.xdmf` in Paraview and pressing `Apply`, one can press the `Warp by vector button` ![Warp by vector](warp_by_vector.png) or go through the top menu (`Filters->Alphabetical->Warp by Vector`) and press `Apply`. We can also change the color of the deformed beam by changing the value in the color menu ![color](color.png) from `Solid Color` to `Deformation`." + "After opening the file `deformation.xdmf` in Paraview and pressing `Apply`,\n", + "one can press the `Warp by vector button` ![Warp by vector](warp_by_vector.png)\n", + "or go through the top menu (`Filters->Alphabetical->Warp by Vector`) and press `Apply`.\n", + "We can also change the color of the deformed beam by changing the value in the\n", + "color menu ![color](color.png) from `Solid Color` to `Deformation`." ] }, { @@ -243,7 +279,9 @@ "metadata": {}, "source": [ "## Stress computation\n", - "As soon as the displacement is computed, we can compute various stress measures. We will compute the von Mises stress defined as $\\sigma_m=\\sqrt{\\frac{3}{2}s:s}$ where $s$ is the deviatoric stress tensor $s(u)=\\sigma(u)-\\frac{1}{3}\\mathrm{tr}(\\sigma(u))I$." + "As soon as the displacement is computed, we can compute various stress measures.\n", + "We will compute the von Mises stress defined as $\\sigma_m=\\sqrt{\\frac{3}{2}s:s}$ where\n", + "$s$ is the deviatoric stress tensor $s(u)=\\sigma(u)-\\frac{1}{3}\\mathrm{tr}(\\sigma(u))I$." ] }, { @@ -252,15 +290,18 @@ "metadata": {}, "outputs": [], "source": [ - "s = sigma(uh) - 1. / 3 * ufl.tr(sigma(uh)) * ufl.Identity(len(uh))\n", - "von_Mises = ufl.sqrt(3. / 2 * ufl.inner(s, s))" + "s = sigma(uh) - 1.0 / 3 * ufl.tr(sigma(uh)) * ufl.Identity(len(uh))\n", + "von_Mises = ufl.sqrt(3.0 / 2 * ufl.inner(s, s))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `von_Mises` variable is now an expression that must be projected into an appropriate function space so that we can visualize it. As `uh` is a linear combination of first order piecewise continuous functions, the von Mises stresses will be a cell-wise constant function." + "The `von_Mises` variable is now an expression that must be projected into an appropriate\n", + "function space so that we can visualize it.\n", + "As `uh` is a linear combination of first order piecewise continuous functions,\n", + "the von Mises stresses will be a cell-wise constant function." ] }, { @@ -279,7 +320,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In the previous sections, we have only visualized first order Lagrangian functions. However, the Von Mises stresses are piecewise constant on each cell. Therefore, we modify our plotting routine slightly. The first thing we notice is that we now set values for each cell, which has a one to one correspondence with the degrees of freedom in the function space." + "In the previous sections, we have only visualized first order Lagrangian functions.\n", + "However, the Von Mises stresses are piecewise constant on each cell.\n", + "Therefore, we modify our plotting routine slightly.\n", + "The first thing we notice is that we now set values for each cell,\n", + "which has a one to one correspondence with the degrees of freedom in the function space." ] }, { @@ -298,7 +343,7 @@ "if not pyvista.OFF_SCREEN:\n", " p.show()\n", "else:\n", - " stress_figure = p.screenshot(f\"stresses.png\")" + " stress_figure = p.screenshot(\"stresses.png\")" ] } ], diff --git a/chapter2/linearelasticity_code.py b/chapter2/linearelasticity_code.py index 6eeff541..ccfe9577 100644 --- a/chapter2/linearelasticity_code.py +++ b/chapter2/linearelasticity_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.6 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -19,40 +19,55 @@ # In this tutorial, you will learn how to: # - Use a vector function space # - Create a constant boundary condition on a vector space -# - Visualize cell wise constant functions +# - Visualize cell-wise constant functions # - Compute Von Mises stresses # # ## Test problem -# As a test example, we will model a clamped beam deformed under its own weigth in 3D. This can be modeled, by setting the right-hand side body force per unit volume to $f=(0,0,-\rho g)$ with $\rho$ the density of the beam and $g$ the acceleration of gravity. The beam is box-shaped with length $L$ and has a square cross section of width $W$. We set $u=u_D=(0,0,0)$ at the clamped end, x=0. The rest of the boundary is traction free, that is, we set $T=0$. We start by defining the physical variables used in the program. +# As a test example, we will model a clamped beam deformed under its own weight in 3D. +# This can be modeled, by setting the right-hand side body force per unit volume to $f=(0,0,-\rho g)$, +# where $\rho$ the density of the beam and $g$ the acceleration of gravity. +# The beam is box-shaped with length $L$ and has a square cross section of width $W$. +# We set $u=u_D=(0,0,0)$ at the clamped end, x=0. The rest of the boundary is traction free, that is, we set $T=0$. +# We start by defining the physical variables used in the program. -# Scaled variable +# + import pyvista from dolfinx import mesh, fem, plot, io, default_scalar_type from dolfinx.fem.petsc import LinearProblem from mpi4py import MPI import ufl import numpy as np -L = 1 + +L = 1.0 W = 0.2 -mu = 1 -rho = 1 +mu = 1.0 +rho = 1.0 delta = W / L gamma = 0.4 * delta**2 beta = 1.25 lambda_ = beta g = gamma +# - # We then create the mesh, which will consist of hexahedral elements, along with the function space. -# As we want a vector element with three components, we add `(3, )` or `(domain.geometry.dim, )` to the element tuple to make it a triplet -# However, we also could have used `basix.ufl`s functionality, creating a vector element `element = basix.ufl.element("Lagrange", domain.topology.cell_name(), 1, shape=(domain.geometry.dim,))`, and initializing the function space as `V = dolfinx.fem.functionspace(domain, element)`. +# As we want a vector element with three components, we add `(3, )` or `(domain.geometry.dim, )` to the element tuple to make it a triplet. +# However, we also could have used `basix.ufl`s functionality, +# creating a vector element `el = basix.ufl.element("Lagrange", domain.basix_cell(), 1, shape=(domain.geometry.dim,))`, +# and initializing the function space as `V = dolfinx.fem.functionspace(domain, el)`. -domain = mesh.create_box(MPI.COMM_WORLD, [np.array([0, 0, 0]), np.array([L, W, W])], - [20, 6, 6], cell_type=mesh.CellType.hexahedron) -V = fem.functionspace(domain, ("Lagrange", 1, (domain.geometry.dim, ))) +domain = mesh.create_box( + MPI.COMM_WORLD, + [np.array([0, 0, 0]), np.array([L, W, W])], + [20, 6, 6], + cell_type=mesh.CellType.hexahedron, +) +V = fem.functionspace(domain, ("Lagrange", 1, (domain.geometry.dim,))) # ## Boundary conditions -# As we would like to clamp the boundary at $x=0$, we do this by using a marker function, which locates the facets where $x$ is close to zero by machine precision. +# As we would like to clamp the boundary at $x=0$, we do this by using a marker function, +# which locates the facets where $x$ is close to zero by machine precision. + # + def clamped_boundary(x): @@ -66,11 +81,12 @@ def clamped_boundary(x): bc = fem.dirichletbc(u_D, fem.locate_dofs_topological(V, fdim, boundary_facets), V) # - -# As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.Constant` +# As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.fem.Constant` T = fem.Constant(domain, default_scalar_type((0, 0, 0))) -# We also want to specify the integration measure $\mathrm{d}s$, which should be the integral over the boundary of our domain. We do this by using `ufl`, and its built in integration measures +# We also want to specify the integration measure $\mathrm{d}s$, which should be the integral over the boundary of our domain. +# We do this by using `ufl`, and its built in integration measures ds = ufl.Measure("ds", domain=domain) @@ -78,9 +94,12 @@ def clamped_boundary(x): # ## Variational formulation # We are now ready to create our variational formulation in close to mathematical syntax, as for the previous problems. + # + def epsilon(u): - return ufl.sym(ufl.grad(u)) # Equivalent to 0.5*(ufl.nabla_grad(u) + ufl.nabla_grad(u).T) + return ufl.sym( + ufl.grad(u) + ) # Equivalent to 0.5*(ufl.nabla_grad(u) + ufl.nabla_grad(u).T) def sigma(u): @@ -99,22 +118,33 @@ def sigma(u): # `div` and `grad`. This is because for scalar functions $\nabla u$ has a clear meaning # $\nabla u = \left(\frac{\partial u}{\partial x}, \frac{\partial u}{\partial y}, \frac{\partial u}{\partial z} \right)$. # -# However, if $u$ is vector valued, the meaning is less clear. Some sources define $\nabla u$ as a matrix with the elements $\frac{\partial u_j}{\partial x_i}$, while other sources prefer -# $\frac{\partial u_i}{\partial x_j}$. In DOLFINx `grad(u)` is defined as the matrix with elements $\frac{\partial u_i}{\partial x_j}$. However, as it is common in continuum mechanics to use the other definition, `ufl` supplies us with `nabla_grad` for this purpose. +# However, if $u$ is vector valued, the meaning is less clear. +# Some sources define $\nabla u$ as a matrix with the elements $\frac{\partial u_j}{\partial x_i}$, while other sources prefer +# $\frac{\partial u_i}{\partial x_j}$. +# In DOLFINx `grad(u)` is defined as the matrix with elements $\frac{\partial u_i}{\partial x_j}$. +# However, as it is common in continuum mechanics to use the other definition, `ufl` supplies us with `nabla_grad` for this purpose. # ``` # # ## Solve the linear variational problem # As in the previous demos, we assemble the matrix and right hand side vector and use PETSc to solve our variational problem -problem = LinearProblem(a, L, bcs=[bc], petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) +problem = LinearProblem( + a, + L, + bcs=[bc], + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="linear_elasticity", +) uh = problem.solve() # ## Visualization -# As in the previous demos, we can either use Pyvista or Paraview for visualization. We start by using Pyvista. Instead of adding scalar values to the grid, we add vectors. +# As in the previous demos, we can either use Pyvista or Paraview for visualization. +# We start by using Pyvista. +# In previous tutorials, we have considered scalar values, while the following section considers vectors. # + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) # Create plotter and pyvista grid p = pyvista.Plotter() @@ -135,7 +165,11 @@ def sigma(u): # We could also use Paraview for visualizing this. # As explained in previous sections, we save the solution with `XDMFFile`. -# After opening the file `deformation.xdmf` in Paraview and pressing `Apply`, one can press the `Warp by vector button` ![Warp by vector](warp_by_vector.png) or go through the top menu (`Filters->Alphabetical->Warp by Vector`) and press `Apply`. We can also change the color of the deformed beam by changing the value in the color menu ![color](color.png) from `Solid Color` to `Deformation`. +# After opening the file `deformation.xdmf` in Paraview and pressing `Apply`, +# one can press the `Warp by vector button` ![Warp by vector](warp_by_vector.png) +# or go through the top menu (`Filters->Alphabetical->Warp by Vector`) and press `Apply`. +# We can also change the color of the deformed beam by changing the value in the +# color menu ![color](color.png) from `Solid Color` to `Deformation`. with io.XDMFFile(domain.comm, "deformation.xdmf", "w") as xdmf: xdmf.write_mesh(domain) @@ -143,19 +177,28 @@ def sigma(u): xdmf.write_function(uh) # ## Stress computation -# As soon as the displacement is computed, we can compute various stress measures. We will compute the von Mises stress defined as $\sigma_m=\sqrt{\frac{3}{2}s:s}$ where $s$ is the deviatoric stress tensor $s(u)=\sigma(u)-\frac{1}{3}\mathrm{tr}(\sigma(u))I$. +# As soon as the displacement is computed, we can compute various stress measures. +# We will compute the von Mises stress defined as $\sigma_m=\sqrt{\frac{3}{2}s:s}$ where +# $s$ is the deviatoric stress tensor $s(u)=\sigma(u)-\frac{1}{3}\mathrm{tr}(\sigma(u))I$. -s = sigma(uh) - 1. / 3 * ufl.tr(sigma(uh)) * ufl.Identity(len(uh)) -von_Mises = ufl.sqrt(3. / 2 * ufl.inner(s, s)) +s = sigma(uh) - 1.0 / 3 * ufl.tr(sigma(uh)) * ufl.Identity(len(uh)) +von_Mises = ufl.sqrt(3.0 / 2 * ufl.inner(s, s)) -# The `von_Mises` variable is now an expression that must be projected into an appropriate function space so that we can visualize it. As `uh` is a linear combination of first order piecewise continuous functions, the von Mises stresses will be a cell-wise constant function. +# The `von_Mises` variable is now an expression that must be projected into an appropriate +# function space so that we can visualize it. +# As `uh` is a linear combination of first order piecewise continuous functions, +# the von Mises stresses will be a cell-wise constant function. V_von_mises = fem.functionspace(domain, ("DG", 0)) stress_expr = fem.Expression(von_Mises, V_von_mises.element.interpolation_points) stresses = fem.Function(V_von_mises) stresses.interpolate(stress_expr) -# In the previous sections, we have only visualized first order Lagrangian functions. However, the Von Mises stresses are piecewise constant on each cell. Therefore, we modify our plotting routine slightly. The first thing we notice is that we now set values for each cell, which has a one to one correspondence with the degrees of freedom in the function space. +# In the previous sections, we have only visualized first order Lagrangian functions. +# However, the Von Mises stresses are piecewise constant on each cell. +# Therefore, we modify our plotting routine slightly. +# The first thing we notice is that we now set values for each cell, +# which has a one to one correspondence with the degrees of freedom in the function space. warped.cell_data["VonMises"] = stresses.x.petsc_vec.array warped.set_active_scalars("VonMises") @@ -165,4 +208,4 @@ def sigma(u): if not pyvista.OFF_SCREEN: p.show() else: - stress_figure = p.screenshot(f"stresses.png") + stress_figure = p.screenshot("stresses.png") diff --git a/chapter2/nonlinpoisson_code.ipynb b/chapter2/nonlinpoisson_code.ipynb index 16c47f80..26eabcc8 100644 --- a/chapter2/nonlinpoisson_code.ipynb +++ b/chapter2/nonlinpoisson_code.ipynb @@ -9,9 +9,13 @@ "Author: Jørgen S. Dokken\n", "\n", "## Test problem\n", - "To solve a test problem, we need to choose the right hand side $f$, the coefficient $q(u)$, and the boundary $u_D$. Previously, we have worked with manufactured solutions that can be reproduced without approximation errors. This is more difficult in nonlinear problems, and the algebra is more tedious. However, we will utilize the UFL differentiation capabilities to obtain a manufactured solution.\n", + "To solve a test problem, we need to choose the right hand side $f$, the coefficient $q(u)$, and the boundary $u_D$.\n", + "Previously, we have worked with manufactured solutions that can be reproduced without approximation errors.\n", + "This is more difficult in nonlinear problems, and the algebra is more tedious.\n", + "However, we will utilize the UFL differentiation capabilities to obtain a manufactured solution.\n", "\n", - "For this problem, we will choose $q(u) = 1 + u^2$ and define a two dimensional manufactured solution that is linear in $x$ and $y$:" + "For this problem, we will choose $q(u) = 1 + u^2$ and define a two dimensional manufactured solution\n", + "that is linear in $x$ and $y$:" ] }, { @@ -26,9 +30,9 @@ "from mpi4py import MPI\n", "from petsc4py import PETSc\n", "\n", - "from dolfinx import mesh, fem, io, nls, log\n", + "from dolfinx import mesh, fem, log\n", "from dolfinx.fem.petsc import NonlinearProblem\n", - "from dolfinx.nls.petsc import NewtonSolver\n", + "\n", "\n", "def q(u):\n", " return 1 + u**2\n", @@ -37,27 +41,42 @@ "domain = mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)\n", "x = ufl.SpatialCoordinate(domain)\n", "u_ufl = 1 + x[0] + 2 * x[1]\n", - "f = - ufl.div(q(u_ufl) * ufl.grad(u_ufl))" + "f = -ufl.div(q(u_ufl) * ufl.grad(u_ufl))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that since `x` is a 2D vector, the first component (index 0) represents $x$, while the second component (index 1) represents $y$. The resulting function `f` can be directly used in variational formulations in DOLFINx.\n", + "Note that since `x` is a 2D vector, the first component (index 0) represents $x$,\n", + "while the second component (index 1) represents $y$.\n", + "The resulting function `f` can be directly used in variational formulations in DOLFINx.\n", "\n", - "As we now have defined our source term and an exact solution, we can create the appropriate function space and boundary conditions.\n", - "Note that as we have already defined the exact solution, we only have to convert it to a Python function that can be evaluated in the interpolation function. We do this by employing the Python `eval` and `lambda`-functions." + "As we now have defined our source term and an exact solution,\n", + "we can create the appropriate function space and boundary conditions.\n", + "Note that as we have already defined the exact solution,\n", + "we only have to convert it to a Python function that can be evaluated in the interpolation function.\n", + "We do this by employing the Python `eval` and `lambda`-functions." ] }, { "cell_type": "code", "execution_count": null, + "id": "57c69c94", "metadata": {}, "outputs": [], "source": [ - "V = fem.functionspace(domain, (\"Lagrange\", 1))\n", - "def u_exact(x): return eval(str(u_ufl))" + "V = fem.functionspace(domain, (\"Lagrange\", 1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def u_exact(x):\n", + " return eval(str(u_ufl))" ] }, { @@ -69,7 +88,9 @@ "u_D = fem.Function(V)\n", "u_D.interpolate(u_exact)\n", "fdim = domain.topology.dim - 1\n", - "boundary_facets = mesh.locate_entities_boundary(domain, fdim, lambda x: numpy.full(x.shape[1], True, dtype=bool))\n", + "boundary_facets = mesh.locate_entities_boundary(\n", + " domain, fdim, lambda x: numpy.full(x.shape[1], True, dtype=bool)\n", + ")\n", "bc = fem.dirichletbc(u_D, fem.locate_dofs_topological(V, fdim, boundary_facets))" ] }, @@ -77,7 +98,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We are now ready to define the variational formulation. Note that as the problem is nonlinear, we have to replace the `TrialFunction` with a `Function`, which serves as the unknown of our problem." + "We are now ready to define the variational formulation.\n", + "Note that as the problem is nonlinear, we have to replace the `TrialFunction` with a `Function`,\n", + "which serves as the unknown of our problem." ] }, { @@ -96,9 +119,19 @@ "metadata": {}, "source": [ "## Newton's method\n", - "The next step is to define the non-linear problem. As it is non-linear we will use [Newtons method](https://en.wikipedia.org/wiki/Newton%27s_method).\n", + "The next step is to define the non-linear problem.\n", + "As it is non-linear we will use [Newtons method](https://en.wikipedia.org/wiki/Newton%27s_method).\n", "For details about how to implement a Newton solver, see [Custom Newton solvers](../chapter4/newton-solver.ipynb).\n", - "Newton's method requires methods for evaluating the residual `F` (including application of boundary conditions), as well as a method for computing the Jacobian matrix. DOLFINx provides the function `NonlinearProblem` that implements these methods. In addition to the boundary conditions, you can supply the variational form for the Jacobian (computed if not supplied), and form and jit parameters, see the [JIT parameters section](../chapter4/compiler_parameters.ipynb)." + "Newton's method requires methods for evaluating the residual `F` (including application of boundary conditions),\n", + "as well as a method for computing the Jacobian matrix.\n", + "DOLFINx provides the function `NonlinearProblem` that implements these methods.\n", + "In addition to the boundary conditions, you can supply the variational form for the Jacobian\n", + "(computed if not supplied), and form and JIT parameters,\n", + "see the [JIT parameters section](../chapter4/compiler_parameters.ipynb).\n", + "The DOLFINx `NonlinearProblem` is an interface to the [PETSc SNES solver](https://petsc.org/release/manual/snes/),\n", + "which provides a large variety of options.\n", + "In this example, we will turn of line-search, to run the problem with a standard Newton method.\n", + "We can also provide PETSc options for the underlying linear solver (KSP) and preconditioner (PC)." ] }, { @@ -107,70 +140,80 @@ "metadata": {}, "outputs": [], "source": [ - "problem = NonlinearProblem(F, uh, bcs=[bc])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, we use the DOLFINx Newton solver. We can set the convergence criteria for the solver by changing the absolute tolerance (`atol`), relative tolerance (`rtol`) or the convergence criterion (`residual` or `incremental`)." + "petsc_options = {\n", + " \"snes_type\": \"newtonls\",\n", + " \"snes_linesearch_type\": \"none\",\n", + " \"snes_atol\": 1e-6,\n", + " \"snes_rtol\": 1e-6,\n", + " \"snes_monitor\": None,\n", + " \"ksp_error_if_not_converged\": True,\n", + " \"ksp_type\": \"gmres\",\n", + " \"ksp_rtol\": 1e-8,\n", + " \"ksp_monitor\": None,\n", + " \"pc_type\": \"hypre\",\n", + " \"pc_hypre_type\": \"boomeramg\",\n", + " \"pc_hypre_boomeramg_max_iter\": 1,\n", + " \"pc_hypre_boomeramg_cycle_type\": \"v\",\n", + "}" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "id": "085a99da", + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ - "solver = NewtonSolver(MPI.COMM_WORLD, problem)\n", - "solver.convergence_criterion = \"incremental\"\n", - "solver.rtol = 1e-6\n", - "solver.report = True" + "problem = NonlinearProblem(\n", + " F,\n", + " uh,\n", + " bcs=[bc],\n", + " petsc_options=petsc_options,\n", + " petsc_options_prefix=\"nonlinpoisson\",\n", + ")" ] }, { "cell_type": "markdown", + "id": "dcbc21ba", "metadata": {}, "source": [ - "We can modify the linear solver in each Newton iteration by accessing the underlying `PETSc` object." + "We are now ready to solve the non-linear problem.\n", + "We assert that the solver has converged and print the number of iterations." ] }, { "cell_type": "code", "execution_count": null, + "id": "a9f33fd0", "metadata": {}, "outputs": [], "source": [ - "ksp = solver.krylov_solver\n", - "opts = PETSc.Options()\n", - "option_prefix = ksp.getOptionsPrefix()\n", - "opts[f\"{option_prefix}ksp_type\"] = \"gmres\"\n", - "opts[f\"{option_prefix}ksp_rtol\"] = 1.0e-8\n", - "opts[f\"{option_prefix}pc_type\"] = \"hypre\"\n", - "opts[f\"{option_prefix}pc_hypre_type\"] = \"boomeramg\"\n", - "opts[f\"{option_prefix}pc_hypre_boomeramg_max_iter\"] = 1\n", - "opts[f\"{option_prefix}pc_hypre_boomeramg_cycle_type\"] = \"v\"\n", - "ksp.setFromOptions()" + "problem.solve()\n", + "converged = problem.solver.getConvergedReason()\n", + "num_iter = problem.solver.getIterationNumber()\n", + "assert converged > 0, \"Solver did not converge, got {converged}.\"\n", + "print(\n", + " f\"Solver converged after {num_iter} iterations with converged reason {converged}.\"\n", + ")" ] }, { "cell_type": "markdown", - "metadata": {}, - "source": [ - "We are now ready to solve the non-linear problem. We assert that the solver has converged and print the number of iterations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ - "log.set_log_level(log.LogLevel.INFO)\n", - "n, converged = solver.solve(uh)\n", - "assert (converged)\n", - "print(f\"Number of interations: {n:d}\")" + "```{admonition} Convergence checks\n", + "We can remove the assertion above, and let PETSc do the error handling by adding\n", + "`snes_error_if_not_converged: True` to the `petsc_options` dictionary.\n", + "This will raise an exception if the solver does not converge.\n", + "We can also set the `snes_atol` and `snes_rtol` or `snes_stol` to control the convergence criteria\n", + "or create custom convergence checks, see [SNES: Convergence checks](https://petsc.org/main/manual/snes/#convergence-tests)\n", + "for more details.\n", + "```" ] }, { @@ -178,7 +221,15 @@ "metadata": {}, "source": [ "We observe that the solver converges after $8$ iterations.\n", - "If we think of the problem in terms of finite differences on a uniform mesh, $\\mathcal{P}_1$ elements mimic standard second-order finite differences, which compute the derivative of a linear or quadratic funtion exactly. Here $\\nabla u$ is a constant vector, which is multiplied by $1+u^2$, giving a second order polynomial in $x$ and $y$, which the finite difference operator would compute exactly. We can therefore, even with $\\mathcal{P}_1$ elements, expect the manufactured solution to be reproduced by the numerical method. However, if we had chosen a nonlinearity, such as $1+u^4$, this would not be the case, and we would need to verify convergence rates." + "If we think of the problem in terms of finite differences on a uniform mesh,\n", + "$\\mathcal{P}_1$ elements mimic standard second-order finite differences,\n", + "which compute the derivative of a linear or quadratic funtion exactly.\n", + "Here $\\nabla u$ is a constant vector, which is multiplied by $1+u^2$,\n", + "giving a second order polynomial in $x$ and $y$, which the finite difference operator would compute exactly.\n", + "We can therefore, even with $\\mathcal{P}_1$ elements, expect the manufactured solution to be\n", + "reproduced by the numerical method.\n", + "However, if we had chosen a nonlinearity, such as $1+u^4$, this would not be the case,\n", + "and we would need to verify convergence rates." ] }, { @@ -193,13 +244,15 @@ "V_ex = fem.functionspace(domain, (\"Lagrange\", 2))\n", "u_ex = fem.Function(V_ex)\n", "u_ex.interpolate(u_exact)\n", - "error_local = fem.assemble_scalar(fem.form((uh - u_ex)**2 * ufl.dx))\n", + "error_local = fem.assemble_scalar(fem.form((uh - u_ex) ** 2 * ufl.dx))\n", "error_L2 = numpy.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))\n", "if domain.comm.rank == 0:\n", " print(f\"L2-error: {error_L2:.2e}\")\n", "\n", "# Compute values at mesh vertices\n", - "error_max = domain.comm.allreduce(numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX)\n", + "error_max = domain.comm.allreduce(\n", + " numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX\n", + ")\n", "if domain.comm.rank == 0:\n", " print(f\"Error_max: {error_max:.2e}\")" ] diff --git a/chapter2/nonlinpoisson_code.py b/chapter2/nonlinpoisson_code.py index f713c623..6d8becbf 100644 --- a/chapter2/nonlinpoisson_code.py +++ b/chapter2/nonlinpoisson_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -18,9 +18,13 @@ # Author: Jørgen S. Dokken # # ## Test problem -# To solve a test problem, we need to choose the right hand side $f$, the coefficient $q(u)$, and the boundary $u_D$. Previously, we have worked with manufactured solutions that can be reproduced without approximation errors. This is more difficult in nonlinear problems, and the algebra is more tedious. However, we will utilize the UFL differentiation capabilities to obtain a manufactured solution. +# To solve a test problem, we need to choose the right hand side $f$, the coefficient $q(u)$, and the boundary $u_D$. +# Previously, we have worked with manufactured solutions that can be reproduced without approximation errors. +# This is more difficult in nonlinear problems, and the algebra is more tedious. +# However, we will utilize the UFL differentiation capabilities to obtain a manufactured solution. # -# For this problem, we will choose $q(u) = 1 + u^2$ and define a two dimensional manufactured solution that is linear in $x$ and $y$: +# For this problem, we will choose $q(u) = 1 + u^2$ and define a two dimensional manufactured solution +# that is linear in $x$ and $y$: # + import ufl @@ -29,9 +33,9 @@ from mpi4py import MPI from petsc4py import PETSc -from dolfinx import mesh, fem, io, nls, log +from dolfinx import mesh, fem, log from dolfinx.fem.petsc import NonlinearProblem -from dolfinx.nls.petsc import NewtonSolver + def q(u): return 1 + u**2 @@ -40,78 +44,127 @@ def q(u): domain = mesh.create_unit_square(MPI.COMM_WORLD, 10, 10) x = ufl.SpatialCoordinate(domain) u_ufl = 1 + x[0] + 2 * x[1] -f = - ufl.div(q(u_ufl) * ufl.grad(u_ufl)) +f = -ufl.div(q(u_ufl) * ufl.grad(u_ufl)) # - -# Note that since `x` is a 2D vector, the first component (index 0) represents $x$, while the second component (index 1) represents $y$. The resulting function `f` can be directly used in variational formulations in DOLFINx. +# Note that since `x` is a 2D vector, the first component (index 0) represents $x$, +# while the second component (index 1) represents $y$. +# The resulting function `f` can be directly used in variational formulations in DOLFINx. # -# As we now have defined our source term and an exact solution, we can create the appropriate function space and boundary conditions. -# Note that as we have already defined the exact solution, we only have to convert it to a Python function that can be evaluated in the interpolation function. We do this by employing the Python `eval` and `lambda`-functions. +# As we now have defined our source term and an exact solution, +# we can create the appropriate function space and boundary conditions. +# Note that as we have already defined the exact solution, +# we only have to convert it to a Python function that can be evaluated in the interpolation function. +# We do this by employing the Python `eval` and `lambda`-functions. V = fem.functionspace(domain, ("Lagrange", 1)) -def u_exact(x): return eval(str(u_ufl)) + + +def u_exact(x): + return eval(str(u_ufl)) u_D = fem.Function(V) u_D.interpolate(u_exact) fdim = domain.topology.dim - 1 -boundary_facets = mesh.locate_entities_boundary(domain, fdim, lambda x: numpy.full(x.shape[1], True, dtype=bool)) +boundary_facets = mesh.locate_entities_boundary( + domain, fdim, lambda x: numpy.full(x.shape[1], True, dtype=bool) +) bc = fem.dirichletbc(u_D, fem.locate_dofs_topological(V, fdim, boundary_facets)) -# We are now ready to define the variational formulation. Note that as the problem is nonlinear, we have to replace the `TrialFunction` with a `Function`, which serves as the unknown of our problem. +# We are now ready to define the variational formulation. +# Note that as the problem is nonlinear, we have to replace the `TrialFunction` with a `Function`, +# which serves as the unknown of our problem. uh = fem.Function(V) v = ufl.TestFunction(V) F = q(uh) * ufl.dot(ufl.grad(uh), ufl.grad(v)) * ufl.dx - f * v * ufl.dx # ## Newton's method -# The next step is to define the non-linear problem. As it is non-linear we will use [Newtons method](https://en.wikipedia.org/wiki/Newton%27s_method). +# The next step is to define the non-linear problem. +# As it is non-linear we will use [Newtons method](https://en.wikipedia.org/wiki/Newton%27s_method). # For details about how to implement a Newton solver, see [Custom Newton solvers](../chapter4/newton-solver.ipynb). -# Newton's method requires methods for evaluating the residual `F` (including application of boundary conditions), as well as a method for computing the Jacobian matrix. DOLFINx provides the function `NonlinearProblem` that implements these methods. In addition to the boundary conditions, you can supply the variational form for the Jacobian (computed if not supplied), and form and jit parameters, see the [JIT parameters section](../chapter4/compiler_parameters.ipynb). - -problem = NonlinearProblem(F, uh, bcs=[bc]) - -# Next, we use the DOLFINx Newton solver. We can set the convergence criteria for the solver by changing the absolute tolerance (`atol`), relative tolerance (`rtol`) or the convergence criterion (`residual` or `incremental`). - -solver = NewtonSolver(MPI.COMM_WORLD, problem) -solver.convergence_criterion = "incremental" -solver.rtol = 1e-6 -solver.report = True - -# We can modify the linear solver in each Newton iteration by accessing the underlying `PETSc` object. - -ksp = solver.krylov_solver -opts = PETSc.Options() -option_prefix = ksp.getOptionsPrefix() -opts[f"{option_prefix}ksp_type"] = "gmres" -opts[f"{option_prefix}ksp_rtol"] = 1.0e-8 -opts[f"{option_prefix}pc_type"] = "hypre" -opts[f"{option_prefix}pc_hypre_type"] = "boomeramg" -opts[f"{option_prefix}pc_hypre_boomeramg_max_iter"] = 1 -opts[f"{option_prefix}pc_hypre_boomeramg_cycle_type"] = "v" -ksp.setFromOptions() - -# We are now ready to solve the non-linear problem. We assert that the solver has converged and print the number of iterations. +# Newton's method requires methods for evaluating the residual `F` (including application of boundary conditions), +# as well as a method for computing the Jacobian matrix. +# DOLFINx provides the function `NonlinearProblem` that implements these methods. +# In addition to the boundary conditions, you can supply the variational form for the Jacobian +# (computed if not supplied), and form and JIT parameters, +# see the [JIT parameters section](../chapter4/compiler_parameters.ipynb). +# The DOLFINx `NonlinearProblem` is an interface to the [PETSc SNES solver](https://petsc.org/release/manual/snes/), +# which provides a large variety of options. +# In this example, we will turn of line-search, to run the problem with a standard Newton method. +# We can also provide PETSc options for the underlying linear solver (KSP) and preconditioner (PC). + +petsc_options = { + "snes_type": "newtonls", + "snes_linesearch_type": "none", + "snes_atol": 1e-6, + "snes_rtol": 1e-6, + "snes_monitor": None, + "ksp_error_if_not_converged": True, + "ksp_type": "gmres", + "ksp_rtol": 1e-8, + "ksp_monitor": None, + "pc_type": "hypre", + "pc_hypre_type": "boomeramg", + "pc_hypre_boomeramg_max_iter": 1, + "pc_hypre_boomeramg_cycle_type": "v", +} + +problem = NonlinearProblem( + F, + uh, + bcs=[bc], + petsc_options=petsc_options, + petsc_options_prefix="nonlinpoisson", +) + + +# We are now ready to solve the non-linear problem. +# We assert that the solver has converged and print the number of iterations. + +problem.solve() +converged = problem.solver.getConvergedReason() +num_iter = problem.solver.getIterationNumber() +assert converged > 0, "Solver did not converge, got {converged}." +print( + f"Solver converged after {num_iter} iterations with converged reason {converged}." +) + +# ```{admonition} Convergence checks +# We can remove the assertion above, and let PETSc do the error handling by adding +# `snes_error_if_not_converged: True` to the `petsc_options` dictionary. +# This will raise an exception if the solver does not converge. +# We can also set the `snes_atol` and `snes_rtol` or `snes_stol` to control the convergence criteria +# or create custom convergence checks, see [SNES: Convergence checks](https://petsc.org/main/manual/snes/#convergence-tests) +# for more details. +# ``` -log.set_log_level(log.LogLevel.INFO) -n, converged = solver.solve(uh) -assert (converged) -print(f"Number of interations: {n:d}") # We observe that the solver converges after $8$ iterations. -# If we think of the problem in terms of finite differences on a uniform mesh, $\mathcal{P}_1$ elements mimic standard second-order finite differences, which compute the derivative of a linear or quadratic funtion exactly. Here $\nabla u$ is a constant vector, which is multiplied by $1+u^2$, giving a second order polynomial in $x$ and $y$, which the finite difference operator would compute exactly. We can therefore, even with $\mathcal{P}_1$ elements, expect the manufactured solution to be reproduced by the numerical method. However, if we had chosen a nonlinearity, such as $1+u^4$, this would not be the case, and we would need to verify convergence rates. +# If we think of the problem in terms of finite differences on a uniform mesh, +# $\mathcal{P}_1$ elements mimic standard second-order finite differences, +# which compute the derivative of a linear or quadratic funtion exactly. +# Here $\nabla u$ is a constant vector, which is multiplied by $1+u^2$, +# giving a second order polynomial in $x$ and $y$, which the finite difference operator would compute exactly. +# We can therefore, even with $\mathcal{P}_1$ elements, expect the manufactured solution to be +# reproduced by the numerical method. +# However, if we had chosen a nonlinearity, such as $1+u^4$, this would not be the case, +# and we would need to verify convergence rates. # + # Compute L2 error and error at nodes V_ex = fem.functionspace(domain, ("Lagrange", 2)) u_ex = fem.Function(V_ex) u_ex.interpolate(u_exact) -error_local = fem.assemble_scalar(fem.form((uh - u_ex)**2 * ufl.dx)) +error_local = fem.assemble_scalar(fem.form((uh - u_ex) ** 2 * ufl.dx)) error_L2 = numpy.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM)) if domain.comm.rank == 0: print(f"L2-error: {error_L2:.2e}") # Compute values at mesh vertices -error_max = domain.comm.allreduce(numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX) +error_max = domain.comm.allreduce( + numpy.max(numpy.abs(uh.x.array - u_D.x.array)), op=MPI.MAX +) if domain.comm.rank == 0: print(f"Error_max: {error_max:.2e}") diff --git a/chapter2/ns_code1.ipynb b/chapter2/ns_code1.ipynb index c60cb5a9..7eab0ae5 100644 --- a/chapter2/ns_code1.ipynb +++ b/chapter2/ns_code1.ipynb @@ -18,32 +18,67 @@ "As we shall see, this problem has an analytical solution.\n", "Let $H$ be the distance between the plates and $L$ the length of the channel. There are no body forces.\n", "\n", - "We may scale the problem first to get rid of seemingly independent physical parameters. The physics of this problem is governed by viscous effects only, in the direction perpendicular to the flow, so a time scale should be based on diffusion across the channel: $t_v=H^2/\\nu$. We let $U$, some characteristic inflow velocity, be the velocity scale and $H$ the spatial scale. The pressure scale is taken as the characteristic shear stress, $\\mu U/H$, since this is a primary example of shear flow. Inserting $\\bar{x}=x/H, \\bar{y}=y/H, \\bar{z}=z/H, \\bar{u}=u/U, \\bar{p}=Hp/{\\mu U}$, and $\\bar{t}=H^2/\\nu$ in the equations results in the scaled Navier-Stokes equations (dropping the bars after scaling)\n", + "We may scale the problem first to get rid of seemingly independent physical parameters.\n", + "The physics of this problem are governed by viscous effects only, in the direction perpendicular to the flow,\n", + "so a time scale should be based on diffusion across the channel: $t_v=H^2/\\nu$.\n", + "We let $U$, some characteristic inflow velocity, be the velocity scale and $H$ the spatial scale.\n", + "The pressure scale is taken as the characteristic shear stress, $\\mu U/H$, as this is a primary example of shear flow.\n", + "Inserting $\\bar{x}=x/H, \\bar{y}=y/H, \\bar{z}=z/H, \\bar{u}=u/U, \\bar{p}=Hp/{\\mu U}$, and $\\bar{t}=H^2/\\nu$\n", + "in the equations results in the scaled Navier-Stokes equations (dropping the bars after scaling)\n", "```{math}\n", ":label: ns-scaled\n", "\\frac{\\partial u}{\\partial t}+ \\mathrm{Re} u \\cdot \\nabla u &= -\\nabla p + \\nabla^2 u,\\\\\n", "\\nabla \\cdot u &=0.\n", "```\n", - "A detailed derivation for scaling of the Navier-Stokes equation for a large variety of physical situations can be found in {cite}`Langtangen2016scaling` (Chapter 4.2) by Hans Petter Langtangen and Geir K. Pedersen.\n", + "A detailed derivation for scaling of the Navier-Stokes equation for a large variety of physical situations\n", + "can be found in {cite}`Langtangen2016scaling` (Chapter 4.2) by Hans Petter Langtangen and Geir K. Pedersen.\n", "\n", - "Here, $\\mathrm{Re}=\\rho UH/\\mu$ is the Reynolds number. Because of the time and pressure scales, which are different from convection dominated fluid flow, the Reynolds number is associated with the convective term and not the viscosity term.\n", + "Here, $\\mathrm{Re}=\\rho UH/\\mu$ is the Reynolds number.\n", + "Because of the time and pressure scales, which are different from convection dominated fluid flow,\n", + "the Reynolds number is associated with the convective term and not the viscosity term.\n", "\n", - "The exact solution is derived by assuming $u=(u_x(x,y,z),0,0)$ with the $x$-axis pointing along the channel. Since $\\nabla \\cdot u = 0$, $u$ cannot be dependent on $x$.\n", + "The exact solution is derived by assuming $u=(u_x(x,y,z),0,0)$ with the $x$-axis pointing along the channel.\n", + "Since $\\nabla \\cdot u = 0$, $u$ cannot be dependent on $x$.\n", "\n", - "The physics of channel flow is also two-dimensional so we can omit the $z$-coordinate (more precisely: $\\partial/\\partial z = 0$). Inserting $u=(u_x, 0, 0)$ in the (scaled) governing equations gives $u_x''(y)=\\frac{\\partial p}{\\partial x}$.\n", + "The physics of channel flow is also two-dimensional so we can omit the $z$-coordinate\n", + "(more precisely: $\\partial/\\partial z = 0$).\n", + "Inserting $u=(u_x, 0, 0)$ in the (scaled) governing equations gives $u_x''(y)=\\frac{\\partial p}{\\partial x}$.\n", "Differentiating this equation with respect to $x$ shows that\n", - "$\\frac{\\partial^2p}{\\partial x^2}=0$ so $\\partial p/\\partial x$ is a constant here called $-\\beta$. This is the driving force of the flow and can be specified as a known parameter in the problem. Integrating $u_x''(x,y)=-\\beta$ over the width of the channel, $[0,1]$, and requiring $u=(0,0,0)$ at the channel walls, results in $u_x=\\frac{1}{2}\\beta y(1-y)$. The characteristic inlet velocity $U$ can be taken as the maximum inflow at $y=0.5$, implying $\\beta=8$. The length of the channel, $L/H$ in the scaled model, has no impact on the result, so for simplicity we just compute on the unit square. Mathematically, the pressure must be prescribed at a point, but since $p$ does not depend on $y$, we can set $p$ to a known value, e.g. zero, along the outlet boundary $x=1$. The result is\n", - "$p(x)=8(1-x)$ and $u_x=4y(1-y)$.\n", - "\n", - "The boundary conditions can be set as $p=8$ at $x=0$, $p=0$ at $x=1$ and $u=(0,0,0)$ on the walls $y=0,1$. This defines the pressure drop and should result in unit maximum velocity at the inlet and outlet and a parabolic velocity profile without no further specifications. Note that it is only meaningful to solve the Navier-Stokes equations in 2D or 3D geometries, although the underlying mathematical problem collapses to two $1D$ problems, one for $u_x(y)$ and one for $p(x)$.\n", - "\n", - "The scaled model is not so easy to simulate using a standard Navier-Stokes solver with dimensions. However, one can argue that the convection term is zero, so the Re coefficient in front of this term in the scaled PDEs is not important and can be set to unity. In that case, setting $\\rho=\\mu=1$ in the original Navier-Stokes equations resembles the scaled model.\n", - "\n", - "For a specific engineering problem one wants to simulate a specific fluid and set corresponding parameters. A general solver is therefore most naturally implemented with dimensions and using the original physical parameters.\n", + "$\\frac{\\partial^2p}{\\partial x^2}=0$ so $\\partial p/\\partial x$ is a constant here called $-\\beta$.\n", + "This is the driving force of the flow and can be specified as a known parameter in the problem.\n", + "Integrating $u_x''(x,y)=-\\beta$ over the width of the channel, $[0,1]$,\n", + "and requiring $u=(0,0,0)$ at the channel walls, results in $u_x=\\frac{1}{2}\\beta y(1-y)$.\n", + "The characteristic inlet velocity $U$ can be taken as the maximum inflow at $y=0.5$, implying $\\beta=8$.\n", + "The length of the channel, $L/H$ in the scaled model, has no impact on the result,\n", + "so for simplicity we just compute on the unit square.\n", + "Mathematically, the pressure must be prescribed at a point, but since $p$ does not depend on $y$,\n", + "we can set $p$ to a known value, e.g. zero, along the outlet boundary $x=1$.\n", + "The result is $p(x)=8(1-x)$ and $u_x=4y(1-y)$.\n", + "\n", + "The boundary conditions can be set as $p=8$ at $x=0$, $p=0$ at $x=1$ and $u=(0,0,0)$ on the walls $y=0,1$.\n", + "This defines the pressure drop and should result in unit maximum velocity at the inlet and outlet and\n", + " a parabolic velocity profile without no further specifications.\n", + "Note that it is only meaningful to solve the Navier-Stokes equations in 2D or 3D geometries,\n", + "although the underlying mathematical problem collapses to two $1D$ problems, one for $u_x(y)$ and one for $p(x)$.\n", + "\n", + "The scaled model is not so easy to simulate using a standard Navier-Stokes solver with dimensions.\n", + "However, one can argue that the convection term is zero, so the Re coefficient in front of this term in\n", + "the scaled PDEs is not important and can be set to unity.\n", + "In that case, setting $\\rho=\\mu=1$ in the original Navier-Stokes equations resembles the scaled model.\n", + "\n", + "For a specific engineering problem one wants to simulate a specific fluid and set corresponding parameters.\n", + "A general solver is therefore most naturally implemented with dimensions and using the original physical parameters.\n", "However, scaling may greatly simplify numerical simulations.\n", - "First of all, it shows that all fluids behave in the same way; it does not matter whether we have oil, gas, or water flowing between two plates, and it does not matter how fast the flow is (up to some critical value of the Reynolds number where the flow becomes unstable and transitions to a complicated turbulent flow of totally different nature.)\n", - " This means that one simulation is enough to cover all types of channel flow!\n", - "In other applications, scaling shows that it might be necessary to just set the fraction of some parameters (dimensionless numbers) rather than the parameters themselves. This simplifies exploring the input parameter space which is often the purpose of simulation. Frequently, the scaled problem is run by setting some of the input parameters with dimension to fixed values (often unity)." + "First of all, it shows that all fluids behave in the same way;\n", + "it does not matter whether we have oil, gas, or water flowing between two plates.\n", + "Secondly, it does not matter how fast the flow is, up to some critical value of the Reynolds number where the\n", + "flow becomes unstable and transitions to a complicated turbulent flow of totally different nature.\n", + "This means that one simulation is enough to cover all types of channel flow!\n", + "In other applications, scaling shows that it might be necessary to just set the fraction of some parameters\n", + "(dimensionless numbers) rather than the parameters themselves.\n", + "This simplifies exploring the input parameter space which is often the purpose of simulation.\n", + "Frequently, the scaled problem is run by setting some of the input parameters with dimension\n", + "to fixed values (often unity)." ] }, { @@ -54,7 +89,8 @@ "\n", "Author: Jørgen S. Dokken\n", "\n", - "As in the previous example, we load the DOLFINx module, along with the `mpi4py` module, and create the unit square mesh and define the run-time and temporal discretization" + "As in the previous example, we load the DOLFINx module, along with the `mpi4py` module,\n", + "and create the unit square mesh and define the run-time and temporal discretization" ] }, { @@ -70,18 +106,45 @@ "import numpy as np\n", "import pyvista\n", "\n", - "from dolfinx.fem import Constant, Function, functionspace, assemble_scalar, dirichletbc, form, locate_dofs_geometrical\n", - "from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, create_vector, set_bc\n", + "from dolfinx.fem import (\n", + " Constant,\n", + " Function,\n", + " functionspace,\n", + " assemble_scalar,\n", + " dirichletbc,\n", + " form,\n", + " locate_dofs_geometrical,\n", + ")\n", + "from dolfinx.fem.petsc import (\n", + " assemble_matrix,\n", + " assemble_vector,\n", + " apply_lifting,\n", + " create_vector,\n", + " set_bc,\n", + ")\n", "from dolfinx.io import VTXWriter\n", "from dolfinx.mesh import create_unit_square\n", "from dolfinx.plot import vtk_mesh\n", "from basix.ufl import element\n", - "from ufl import (FacetNormal, Identity, TestFunction, TrialFunction,\n", - " div, dot, ds, dx, inner, lhs, nabla_grad, rhs, sym)\n", + "from ufl import (\n", + " FacetNormal,\n", + " Identity,\n", + " TestFunction,\n", + " TrialFunction,\n", + " div,\n", + " dot,\n", + " ds,\n", + " dx,\n", + " inner,\n", + " lhs,\n", + " nabla_grad,\n", + " rhs,\n", + " sym,\n", + ")\n", "\n", "mesh = create_unit_square(MPI.COMM_WORLD, 10, 10)\n", - "t = 0\n", - "T = 10\n", + "t = 0.0\n", + "T = 10.0\n", "num_steps = 500\n", "dt = T / num_steps" ] @@ -101,8 +164,8 @@ }, "outputs": [], "source": [ - "v_cg2 = element(\"Lagrange\", mesh.topology.cell_name(), 2, shape=(mesh.geometry.dim, ))\n", - "s_cg1 = element(\"Lagrange\", mesh.topology.cell_name(), 1)\n", + "v_cg2 = element(\"Lagrange\", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,))\n", + "s_cg1 = element(\"Lagrange\", mesh.basix_cell(), 1)\n", "V = functionspace(mesh, v_cg2)\n", "Q = functionspace(mesh, s_cg1)" ] @@ -111,23 +174,35 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The first space `V` is a vector valued function space for the velocity, while `Q` is a scalar valued function space for pressure. We use piecewise quadratic elements for the velocity and piecewise linear elements for the pressure. When creating the vector finite element, the dimension of the vector element will be set to the geometric dimension of the mesh. One can easily create vector-valued function spaces with other dimensions by replacing `(mesh.geometry.dim, )` with something else, like\n", + "The first space `V` is a vector valued function space for the velocity,\n", + "while `Q` is a scalar valued function space for pressure.\n", + "We use piecewise quadratic elements for the velocity and piecewise linear elements for the pressure.\n", + "One can easily create vector-valued function spaces with other dimensions by replacing\n", + "`shape=(mesh.geometry.dim, )` with something else, like\n", "```\n", - "v_cg basix.ufl.element(\"Lagrange\", mesh.topology.cell_name(), 2, shape=(10,))\n", + "v_cg basix.ufl.element(\"Lagrange\", mesh.basix_cell(), 2, shape=(10,))\n", "```\n", - "or \n", + "or\n", "```\n", - "tensor_element = basix.ufl.element(\"Lagrange\", mesh.topology.cell_name(), 2, shape=(3, 3))\n", + "tensor_element = basix.ufl.element(\"Lagrange\", mesh.basix_cell(), 2, shape=(3, 3))\n", "```\n", "or\n", "```\n", - "tensor_element = basix.ufl.element(\"Lagrange\", mesh.topology.cell_name(), 2, shape=(3, 2, 4))\n", + "tensor_element = basix.ufl.element(\"Lagrange\", mesh.basix_cell(), 2, shape=(3, 2, 4))\n", "```\n", "\n", "\n", "```{admonition} Stable finite element spaces for the Navier-Stokes equation\n", - "It is well-known that certain finite element spaces are not *stable* for the Navier-Stokes equations, or even for the simpler Stokes equation. The prime example of an unstable pair of finite element spaces is to use first order degree continuous piecewise polynomials for both the velocity and the pressure.\n", - "Using an unstable pair of spaces typically results in a solution with *spurious* (unwanted, non-physical) oscillations in the pressure solution. The simple remedy is to use continuous piecewise quadratic elements for the velocity and continuous piecewise linear elements for the pressure. Together, these elements form the so-called *Taylor-Hood* element. Spurious oscillations may occur also for splitting methods if an unstable element pair is used.\n", + "It is well-known that certain finite element spaces are not *stable* for the Navier-Stokes equations,\n", + "or even for the simpler Stokes equation.\n", + "The prime example of an unstable pair of finite element spaces is to use first order degree continuous\n", + "piecewise polynomials for both the velocity and the pressure.\n", + "Using an unstable pair of spaces typically results in a solution with *spurious* (unwanted, non-physical)\n", + "oscillations in the pressure solution.\n", + "The simple remedy is to use continuous piecewise quadratic elements for the velocity and continuous\n", + "piecewise linear elements for the pressure.\n", + "Together, these elements form the so-called *Taylor-Hood* element.\n", + "Spurious oscillations may occur also for splitting methods if an unstable element pair is used.\n", "\n", "Since we have two different function spaces, we need to create two sets of trial and test functions:" ] @@ -135,7 +210,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "u = TrialFunction(V)\n", @@ -146,9 +223,15 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ - "As we have seen in [Linear elasticity problem](./linearelasticity_code) we can use Python-functions to create the different Dirichlet conditions. For this problem, we have three Dirichlet condition: First, we will set $u=0$ at the walls of the channel, that is at $y=0$ and $y=1$. In this case, we will use `dolfinx.fem.locate_dofs_geometrical`" + "As we have seen in [Linear elasticity problem](./linearelasticity_code) we can use Python-functions\n", + "to create the different Dirichlet conditions.\n", + "For this problem, we have three Dirichlet condition:\n", + "First, we will set $u=0$ at the walls of the channel, that is at $y=0$ and $y=1$.\n", + "In this case, we will use `dolfinx.fem.locate_dofs_geometrical`" ] }, { @@ -158,15 +241,9 @@ "outputs": [], "source": [ "def walls(x):\n", - " return np.logical_or(np.isclose(x[1], 0), np.isclose(x[1], 1))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + " return np.logical_or(np.isclose(x[1], 0), np.isclose(x[1], 1))\n", + "\n", + "\n", "wall_dofs = locate_dofs_geometrical(V, walls)\n", "u_noslip = np.array((0,) * mesh.geometry.dim, dtype=PETSc.ScalarType)\n", "bc_noslip = dirichletbc(u_noslip, wall_dofs, V)" @@ -174,7 +251,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "Second, we will set $p=8$ at the inflow ($x=0$)" ] @@ -186,42 +265,37 @@ "outputs": [], "source": [ "def inflow(x):\n", - " return np.isclose(x[0], 0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + " return np.isclose(x[0], 0)\n", + "\n", + "\n", "inflow_dofs = locate_dofs_geometrical(Q, inflow)\n", "bc_inflow = dirichletbc(PETSc.ScalarType(8), inflow_dofs, Q)" ] }, { "cell_type": "markdown", - "metadata": {}, + "id": "6e372796", + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ - "And finally, $p=0$ at the outflow ($x=1$). This will result in a pressure gradient that will accelerate the flow from the initial state with zero velocity. At the end, we collect the boundary conditions for the velocity and pressure in Python lists so we can easily access them in the following computation." + "And finally, $p=0$ at the outflow ($x=1$).\n", + "This will result in a pressure gradient that will accelerate the flow from the initial state with zero velocity.\n", + "At the end, we collect the boundary conditions for the velocity and pressure in Python lists so we\n", + "can easily access them in the following computation." ] }, { "cell_type": "code", "execution_count": null, + "id": "e8881e81", "metadata": {}, "outputs": [], "source": [ "def outflow(x):\n", - " return np.isclose(x[0], 1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + " return np.isclose(x[0], 1)\n", + "\n", + "\n", "outflow_dofs = locate_dofs_geometrical(Q, outflow)\n", "bc_outflow = dirichletbc(PETSc.ScalarType(0), outflow_dofs, Q)\n", "bcu = [bc_noslip]\n", @@ -230,15 +304,20 @@ }, { "cell_type": "markdown", + "id": "354c51c7", "metadata": {}, "source": [ - "We now move on to the definition of the three variational forms, one for each step in the IPCS scheme. Let us look at the definition of the first variational problem and the relevant parameters." + "We now move on to the definition of the three variational forms, one for each step in the IPCS scheme.\n", + "Let us look at the definition of the first variational problem and the relevant parameters." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "id": "37563ee6", + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "u_n = Function(V)\n", @@ -253,33 +332,55 @@ }, { "cell_type": "markdown", - "metadata": {}, + "id": "a4a1c349", + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ - "```{admonition} Usage of \"dolfinx.Constant\"\n", - "Note that we have wrapped several parameters as constants. This is to reduce the compilation-time of the variational formulations. By wrapping them as a constant, we can change the variable\n", + "```{admonition} Usage of \"dolfinx.fem.Constant\"\n", + "Note that we have wrapped several parameters as constants.\n", + "This is to reduce the compilation-time of the variational formulations.\n", + "By wrapping them as a constant, we can change the variable\n", "```\n", "The next step is to set up the variational form of the first step.\n", - "As the variational problem contains a mix of known and unknown quantities, we will use the following naming convention: `u` (mathematically $u^{n+1}$) is known as a trial function in the variational form. `u_` is the most recently computed approximation ($u^{n+1}$ available as a `Function` object), `u_n` is $u^n$, and the same convention goes for `p,p_` ($p^{n+1}$) and `p_n` (p^n)." + "As the variational problem contains a mix of known and unknown quantities,\n", + "we will use the following naming convention: `u` (mathematically $u^{n+1}$) is known as a trial function\n", + "in the variational form. `u_` is the most recently computed approximation\n", + "($u^{n+1}$ available as a `Function` object), `u_n` is $u^n$, and the same convention\n", + "goes for `p,p_` ($p^{n+1}$) and `p_n` (p^n)." ] }, { "cell_type": "code", "execution_count": null, + "id": "63305ff6", "metadata": {}, "outputs": [], "source": [ - "# Define strain-rate tensor\n", "def epsilon(u):\n", + " \"\"\"Strain-rate tensor.\"\"\"\n", " return sym(nabla_grad(u))\n", "\n", - "# Define stress tensor\n", - "\n", "\n", "def sigma(u, p):\n", - " return 2 * mu * epsilon(u) - p * Identity(len(u))\n", - "\n", - "\n", - "# Define the variational problem for the first step\n", + " \"\"\"Stress tensor.\"\"\"\n", + " return 2 * mu * epsilon(u) - p * Identity(len(u))" + ] + }, + { + "cell_type": "markdown", + "id": "9ba83a2a", + "metadata": {}, + "source": [ + "Define the variational problem for the first step" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "p_n = Function(Q)\n", "p_n.name = \"p_n\"\n", "F1 = rho * dot((u - u_n) / k, v) * dx\n", @@ -295,7 +396,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that we have used the `ufl`-functions `lhs` and `rhs` to sort out the bilinear form $a(u,v)$ and linear form $L(v)$. This is particulary convenient in longer and more complicated variational forms. With our particular discretization $a(u,v)$ `a1` is not time dependent, and only has to be assembled once, while the right hand side is dependent on the solution from the previous time step (`u_n`). Thus, we do as for the [](./heat_code), and create the matrix outside the time-loop." + "Note that we have used the `ufl`-functions `lhs` and `rhs` to sort out the bilinear form\n", + "$a(u,v)$ and linear form $L(v)$.\n", + "This is particulary convenient in longer and more complicated variational forms.\n", + "With our particular discretization $a(u,v)$ `a1` is not time dependent,\n", + "and only has to be assembled once, while the right hand side is dependent on the solution\n", + "from the previous time step (`u_n`).\n", + "Thus, we do as for the [](./heat_code), and create the matrix outside the time-loop." ] }, { @@ -343,7 +450,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As we have create all the linear structures for the problem, we can now create a solver for each of them using PETSc. We can therefore customize the solution strategy for each step. For the tentative velocity step and pressure correction step, we will use the Stabilized version of BiConjugate Gradient to solve the linear system, and using algebraic multigrid for preconditioning. For the last step, the velocity update, we use a conjugate gradient method with successive over relaxation, Gauss Seidel (SOR) preconditioning." + "As we have create all the linear structures for the problem, we can now create a solver for each of them using PETSc.\n", + "We can therefore customize the solution strategy for each step.\n", + "For the tentative velocity step and pressure correction step,\n", + "we will use the Stabilized version of BiConjugate Gradient to solve the linear system,\n", + "and using algebraic multigrid for preconditioning.\n", + "For the last step, the velocity update, we use a conjugate gradient method with successive over relaxation,\n", + "Gauss Seidel (SOR) preconditioning." ] }, { @@ -386,10 +499,12 @@ { "cell_type": "code", "execution_count": null, + "id": "c8a5b064", "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", + "\n", "folder = Path(\"results\")\n", "folder.mkdir(exist_ok=True, parents=True)\n", "vtx_u = VTXWriter(mesh.comm, folder / \"poiseuille_u.bp\", u_n, engine=\"BP4\")\n", @@ -481,7 +596,9 @@ "\n", " # Compute error at current time-step\n", " error_L2 = np.sqrt(mesh.comm.allreduce(assemble_scalar(L2_error), op=MPI.SUM))\n", - " error_max = mesh.comm.allreduce(np.max(u_.x.petsc_vec.array - u_ex.x.petsc_vec.array), op=MPI.MAX)\n", + " error_max = mesh.comm.allreduce(\n", + " np.max(u_.x.petsc_vec.array - u_ex.x.petsc_vec.array), op=MPI.MAX\n", + " )\n", " # Print error only every 20th step and at the last step\n", " if (i % 20 == 0) or (i == num_steps - 1):\n", " print(f\"Time {t:.2f}, L2-error {error_L2:.2e}, Max error {error_max:.2e}\")\n", @@ -498,25 +615,34 @@ }, { "cell_type": "markdown", + "id": "7f52229b", "metadata": {}, "source": [ "## Verification\n", - "As for the previous problems we compute the error at each degree of freedom and the $L^2(\\Omega)$-error. We start with the initial condition $u=(0,0)$. We have not specified the initial condition explicitly, and FEniCSx will initialize all `Function`s including `u_n` and `u_` to zero. Since the exact solution is quadratic, we expect to reach machine precision within finite time. For our implementation, we observe that the error quickly approaches zero, and is of order $10^{-6}$ at $T=10$\n", + "As for the previous problems we compute the error at each degree of freedom and the $L^2(\\Omega)$-error.\n", + "We start with the initial condition $u=(0,0)$.\n", + "We have not specified the initial condition explicitly, and FEniCSx will initialize all\n", + "`Function`s including `u_n` and `u_` to zero.\n", + "Since the exact solution is quadratic, we expect to reach machine precision within finite time.\n", + "For our implementation, we observe that the error quickly approaches zero, and is of order $10^{-6}$ at $T=10$\n", "\n", "## Visualization of vectors\n", - "We have already looked at how to plot higher order functions and vector functions. In this section we will look at how to visualize vector functions with glyphs, instead of warping the mesh." + "We have already looked at how to plot higher order functions and vector functions.\n", + "In this section we will look at how to visualize vector functions with glyphs, instead of warping the mesh." ] }, { "cell_type": "code", "execution_count": null, + "id": "fd51572b", "metadata": {}, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", + "\n", "topology, cell_types, geometry = vtk_mesh(V)\n", "values = np.zeros((geometry.shape[0], 3), dtype=np.float64)\n", - "values[:, :len(u_n)] = u_n.x.array.real.reshape((geometry.shape[0], len(u_n)))\n", + "values[:, : len(u_n)] = u_n.x.array.real.reshape((geometry.shape[0], len(u_n)))\n", "\n", "# Create a point cloud of glyphs\n", "function_grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)\n", @@ -524,14 +650,16 @@ "glyphs = function_grid.glyph(orient=\"u\", factor=0.2)\n", "\n", "# Create a pyvista-grid for the mesh\n", - "mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim)\n", - "grid = pyvista.UnstructuredGrid(*vtk_mesh(mesh, mesh.topology.dim))\n", + "tdim = mesh.topology.dim\n", + "mesh.topology.create_connectivity(tdim, tdim)\n", + "grid = pyvista.UnstructuredGrid(*vtk_mesh(mesh, tdim))\n", "\n", "# Create plotter\n", "plotter = pyvista.Plotter()\n", "plotter.add_mesh(grid, style=\"wireframe\", color=\"k\")\n", "plotter.add_mesh(glyphs)\n", "plotter.view_xy()\n", + "\n", "if not pyvista.OFF_SCREEN:\n", " plotter.show()\n", "else:\n", diff --git a/chapter2/ns_code1.py b/chapter2/ns_code1.py index bb235bd7..42b98c38 100644 --- a/chapter2/ns_code1.py +++ b/chapter2/ns_code1.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -27,38 +27,74 @@ # As we shall see, this problem has an analytical solution. # Let $H$ be the distance between the plates and $L$ the length of the channel. There are no body forces. # -# We may scale the problem first to get rid of seemingly independent physical parameters. The physics of this problem is governed by viscous effects only, in the direction perpendicular to the flow, so a time scale should be based on diffusion across the channel: $t_v=H^2/\nu$. We let $U$, some characteristic inflow velocity, be the velocity scale and $H$ the spatial scale. The pressure scale is taken as the characteristic shear stress, $\mu U/H$, since this is a primary example of shear flow. Inserting $\bar{x}=x/H, \bar{y}=y/H, \bar{z}=z/H, \bar{u}=u/U, \bar{p}=Hp/{\mu U}$, and $\bar{t}=H^2/\nu$ in the equations results in the scaled Navier-Stokes equations (dropping the bars after scaling) +# We may scale the problem first to get rid of seemingly independent physical parameters. +# The physics of this problem are governed by viscous effects only, in the direction perpendicular to the flow, +# so a time scale should be based on diffusion across the channel: $t_v=H^2/\nu$. +# We let $U$, some characteristic inflow velocity, be the velocity scale and $H$ the spatial scale. +# The pressure scale is taken as the characteristic shear stress, $\mu U/H$, as this is a primary example of shear flow. +# Inserting $\bar{x}=x/H, \bar{y}=y/H, \bar{z}=z/H, \bar{u}=u/U, \bar{p}=Hp/{\mu U}$, and $\bar{t}=H^2/\nu$ +# in the equations results in the scaled Navier-Stokes equations (dropping the bars after scaling) # ```{math} # :label: ns-scaled # \frac{\partial u}{\partial t}+ \mathrm{Re} u \cdot \nabla u &= -\nabla p + \nabla^2 u,\\ # \nabla \cdot u &=0. # ``` -# A detailed derivation for scaling of the Navier-Stokes equation for a large variety of physical situations can be found in {cite}`Langtangen2016scaling` (Chapter 4.2) by Hans Petter Langtangen and Geir K. Pedersen. +# A detailed derivation for scaling of the Navier-Stokes equation for a large variety of physical situations +# can be found in {cite}`Langtangen2016scaling` (Chapter 4.2) by Hans Petter Langtangen and Geir K. Pedersen. # -# Here, $\mathrm{Re}=\rho UH/\mu$ is the Reynolds number. Because of the time and pressure scales, which are different from convection dominated fluid flow, the Reynolds number is associated with the convective term and not the viscosity term. +# Here, $\mathrm{Re}=\rho UH/\mu$ is the Reynolds number. +# Because of the time and pressure scales, which are different from convection dominated fluid flow, +# the Reynolds number is associated with the convective term and not the viscosity term. # -# The exact solution is derived by assuming $u=(u_x(x,y,z),0,0)$ with the $x$-axis pointing along the channel. Since $\nabla \cdot u = 0$, $u$ cannot be dependent on $x$. +# The exact solution is derived by assuming $u=(u_x(x,y,z),0,0)$ with the $x$-axis pointing along the channel. +# Since $\nabla \cdot u = 0$, $u$ cannot be dependent on $x$. # -# The physics of channel flow is also two-dimensional so we can omit the $z$-coordinate (more precisely: $\partial/\partial z = 0$). Inserting $u=(u_x, 0, 0)$ in the (scaled) governing equations gives $u_x''(y)=\frac{\partial p}{\partial x}$. +# The physics of channel flow is also two-dimensional so we can omit the $z$-coordinate +# (more precisely: $\partial/\partial z = 0$). +# Inserting $u=(u_x, 0, 0)$ in the (scaled) governing equations gives $u_x''(y)=\frac{\partial p}{\partial x}$. # Differentiating this equation with respect to $x$ shows that -# $\frac{\partial^2p}{\partial x^2}=0$ so $\partial p/\partial x$ is a constant here called $-\beta$. This is the driving force of the flow and can be specified as a known parameter in the problem. Integrating $u_x''(x,y)=-\beta$ over the width of the channel, $[0,1]$, and requiring $u=(0,0,0)$ at the channel walls, results in $u_x=\frac{1}{2}\beta y(1-y)$. The characteristic inlet velocity $U$ can be taken as the maximum inflow at $y=0.5$, implying $\beta=8$. The length of the channel, $L/H$ in the scaled model, has no impact on the result, so for simplicity we just compute on the unit square. Mathematically, the pressure must be prescribed at a point, but since $p$ does not depend on $y$, we can set $p$ to a known value, e.g. zero, along the outlet boundary $x=1$. The result is -# $p(x)=8(1-x)$ and $u_x=4y(1-y)$. +# $\frac{\partial^2p}{\partial x^2}=0$ so $\partial p/\partial x$ is a constant here called $-\beta$. +# This is the driving force of the flow and can be specified as a known parameter in the problem. +# Integrating $u_x''(x,y)=-\beta$ over the width of the channel, $[0,1]$, +# and requiring $u=(0,0,0)$ at the channel walls, results in $u_x=\frac{1}{2}\beta y(1-y)$. +# The characteristic inlet velocity $U$ can be taken as the maximum inflow at $y=0.5$, implying $\beta=8$. +# The length of the channel, $L/H$ in the scaled model, has no impact on the result, +# so for simplicity we just compute on the unit square. +# Mathematically, the pressure must be prescribed at a point, but since $p$ does not depend on $y$, +# we can set $p$ to a known value, e.g. zero, along the outlet boundary $x=1$. +# The result is $p(x)=8(1-x)$ and $u_x=4y(1-y)$. # -# The boundary conditions can be set as $p=8$ at $x=0$, $p=0$ at $x=1$ and $u=(0,0,0)$ on the walls $y=0,1$. This defines the pressure drop and should result in unit maximum velocity at the inlet and outlet and a parabolic velocity profile without no further specifications. Note that it is only meaningful to solve the Navier-Stokes equations in 2D or 3D geometries, although the underlying mathematical problem collapses to two $1D$ problems, one for $u_x(y)$ and one for $p(x)$. +# The boundary conditions can be set as $p=8$ at $x=0$, $p=0$ at $x=1$ and $u=(0,0,0)$ on the walls $y=0,1$. +# This defines the pressure drop and should result in unit maximum velocity at the inlet and outlet and +# a parabolic velocity profile without no further specifications. +# Note that it is only meaningful to solve the Navier-Stokes equations in 2D or 3D geometries, +# although the underlying mathematical problem collapses to two $1D$ problems, one for $u_x(y)$ and one for $p(x)$. # -# The scaled model is not so easy to simulate using a standard Navier-Stokes solver with dimensions. However, one can argue that the convection term is zero, so the Re coefficient in front of this term in the scaled PDEs is not important and can be set to unity. In that case, setting $\rho=\mu=1$ in the original Navier-Stokes equations resembles the scaled model. +# The scaled model is not so easy to simulate using a standard Navier-Stokes solver with dimensions. +# However, one can argue that the convection term is zero, so the Re coefficient in front of this term in +# the scaled PDEs is not important and can be set to unity. +# In that case, setting $\rho=\mu=1$ in the original Navier-Stokes equations resembles the scaled model. # -# For a specific engineering problem one wants to simulate a specific fluid and set corresponding parameters. A general solver is therefore most naturally implemented with dimensions and using the original physical parameters. +# For a specific engineering problem one wants to simulate a specific fluid and set corresponding parameters. +# A general solver is therefore most naturally implemented with dimensions and using the original physical parameters. # However, scaling may greatly simplify numerical simulations. -# First of all, it shows that all fluids behave in the same way; it does not matter whether we have oil, gas, or water flowing between two plates, and it does not matter how fast the flow is (up to some critical value of the Reynolds number where the flow becomes unstable and transitions to a complicated turbulent flow of totally different nature.) -# This means that one simulation is enough to cover all types of channel flow! -# In other applications, scaling shows that it might be necessary to just set the fraction of some parameters (dimensionless numbers) rather than the parameters themselves. This simplifies exploring the input parameter space which is often the purpose of simulation. Frequently, the scaled problem is run by setting some of the input parameters with dimension to fixed values (often unity). +# First of all, it shows that all fluids behave in the same way; +# it does not matter whether we have oil, gas, or water flowing between two plates. +# Secondly, it does not matter how fast the flow is, up to some critical value of the Reynolds number where the +# flow becomes unstable and transitions to a complicated turbulent flow of totally different nature. +# This means that one simulation is enough to cover all types of channel flow! +# In other applications, scaling shows that it might be necessary to just set the fraction of some parameters +# (dimensionless numbers) rather than the parameters themselves. +# This simplifies exploring the input parameter space which is often the purpose of simulation. +# Frequently, the scaled problem is run by setting some of the input parameters with dimension +# to fixed values (often unity). # ## Implementation # # Author: Jørgen S. Dokken # -# As in the previous example, we load the DOLFINx module, along with the `mpi4py` module, and create the unit square mesh and define the run-time and temporal discretization +# As in the previous example, we load the DOLFINx module, along with the `mpi4py` module, +# and create the unit square mesh and define the run-time and temporal discretization # + from mpi4py import MPI @@ -66,46 +102,85 @@ import numpy as np import pyvista -from dolfinx.fem import Constant, Function, functionspace, assemble_scalar, dirichletbc, form, locate_dofs_geometrical -from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, create_vector, set_bc +from dolfinx.fem import ( + Constant, + Function, + functionspace, + assemble_scalar, + dirichletbc, + form, + locate_dofs_geometrical, +) +from dolfinx.fem.petsc import ( + assemble_matrix, + assemble_vector, + apply_lifting, + create_vector, + set_bc, +) from dolfinx.io import VTXWriter from dolfinx.mesh import create_unit_square from dolfinx.plot import vtk_mesh from basix.ufl import element -from ufl import (FacetNormal, Identity, TestFunction, TrialFunction, - div, dot, ds, dx, inner, lhs, nabla_grad, rhs, sym) +from ufl import ( + FacetNormal, + Identity, + TestFunction, + TrialFunction, + div, + dot, + ds, + dx, + inner, + lhs, + nabla_grad, + rhs, + sym, +) mesh = create_unit_square(MPI.COMM_WORLD, 10, 10) -t = 0 -T = 10 +t = 0.0 +T = 10.0 num_steps = 500 dt = T / num_steps # - # As opposed to the previous demos, we will create our two function spaces using the `ufl` element definitions as input -v_cg2 = element("Lagrange", mesh.topology.cell_name(), 2, shape=(mesh.geometry.dim, )) -s_cg1 = element("Lagrange", mesh.topology.cell_name(), 1) +v_cg2 = element("Lagrange", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,)) +s_cg1 = element("Lagrange", mesh.basix_cell(), 1) V = functionspace(mesh, v_cg2) Q = functionspace(mesh, s_cg1) -# The first space `V` is a vector valued function space for the velocity, while `Q` is a scalar valued function space for pressure. We use piecewise quadratic elements for the velocity and piecewise linear elements for the pressure. When creating the vector finite element, the dimension of the vector element will be set to the geometric dimension of the mesh. One can easily create vector-valued function spaces with other dimensions by replacing `(mesh.geometry.dim, )` with something else, like +# The first space `V` is a vector valued function space for the velocity, +# while `Q` is a scalar valued function space for pressure. +# We use piecewise quadratic elements for the velocity and piecewise linear elements for the pressure. +# One can easily create vector-valued function spaces with other dimensions by replacing +# `shape=(mesh.geometry.dim, )` with something else, like # ``` -# v_cg basix.ufl.element("Lagrange", mesh.topology.cell_name(), 2, shape=(10,)) +# v_cg basix.ufl.element("Lagrange", mesh.basix_cell(), 2, shape=(10,)) # ``` -# or +# or # ``` -# tensor_element = basix.ufl.element("Lagrange", mesh.topology.cell_name(), 2, shape=(3, 3)) +# tensor_element = basix.ufl.element("Lagrange", mesh.basix_cell(), 2, shape=(3, 3)) # ``` # or # ``` -# tensor_element = basix.ufl.element("Lagrange", mesh.topology.cell_name(), 2, shape=(3, 2, 4)) +# tensor_element = basix.ufl.element("Lagrange", mesh.basix_cell(), 2, shape=(3, 2, 4)) # ``` # # # ```{admonition} Stable finite element spaces for the Navier-Stokes equation -# It is well-known that certain finite element spaces are not *stable* for the Navier-Stokes equations, or even for the simpler Stokes equation. The prime example of an unstable pair of finite element spaces is to use first order degree continuous piecewise polynomials for both the velocity and the pressure. -# Using an unstable pair of spaces typically results in a solution with *spurious* (unwanted, non-physical) oscillations in the pressure solution. The simple remedy is to use continuous piecewise quadratic elements for the velocity and continuous piecewise linear elements for the pressure. Together, these elements form the so-called *Taylor-Hood* element. Spurious oscillations may occur also for splitting methods if an unstable element pair is used. +# It is well-known that certain finite element spaces are not *stable* for the Navier-Stokes equations, +# or even for the simpler Stokes equation. +# The prime example of an unstable pair of finite element spaces is to use first order degree continuous +# piecewise polynomials for both the velocity and the pressure. +# Using an unstable pair of spaces typically results in a solution with *spurious* (unwanted, non-physical) +# oscillations in the pressure solution. +# The simple remedy is to use continuous piecewise quadratic elements for the velocity and continuous +# piecewise linear elements for the pressure. +# Together, these elements form the so-called *Taylor-Hood* element. +# Spurious oscillations may occur also for splitting methods if an unstable element pair is used. # # Since we have two different function spaces, we need to create two sets of trial and test functions: @@ -115,8 +190,14 @@ q = TestFunction(Q) -# As we have seen in [Linear elasticity problem](./linearelasticity_code) we can use Python-functions to create the different Dirichlet conditions. For this problem, we have three Dirichlet condition: First, we will set $u=0$ at the walls of the channel, that is at $y=0$ and $y=1$. In this case, we will use `dolfinx.fem.locate_dofs_geometrical` +# As we have seen in [Linear elasticity problem](./linearelasticity_code) we can use Python-functions +# to create the different Dirichlet conditions. +# For this problem, we have three Dirichlet condition: +# First, we will set $u=0$ at the walls of the channel, that is at $y=0$ and $y=1$. +# In this case, we will use `dolfinx.fem.locate_dofs_geometrical` + +# + def walls(x): return np.logical_or(np.isclose(x[1], 0), np.isclose(x[1], 1)) @@ -124,20 +205,27 @@ def walls(x): wall_dofs = locate_dofs_geometrical(V, walls) u_noslip = np.array((0,) * mesh.geometry.dim, dtype=PETSc.ScalarType) bc_noslip = dirichletbc(u_noslip, wall_dofs, V) - +# - # Second, we will set $p=8$ at the inflow ($x=0$) + +# + def inflow(x): return np.isclose(x[0], 0) inflow_dofs = locate_dofs_geometrical(Q, inflow) bc_inflow = dirichletbc(PETSc.ScalarType(8), inflow_dofs, Q) +# - +# And finally, $p=0$ at the outflow ($x=1$). +# This will result in a pressure gradient that will accelerate the flow from the initial state with zero velocity. +# At the end, we collect the boundary conditions for the velocity and pressure in Python lists so we +# can easily access them in the following computation. -# And finally, $p=0$ at the outflow ($x=1$). This will result in a pressure gradient that will accelerate the flow from the initial state with zero velocity. At the end, we collect the boundary conditions for the velocity and pressure in Python lists so we can easily access them in the following computation. +# + def outflow(x): return np.isclose(x[0], 1) @@ -146,8 +234,10 @@ def outflow(x): bc_outflow = dirichletbc(PETSc.ScalarType(0), outflow_dofs, Q) bcu = [bc_noslip] bcp = [bc_inflow, bc_outflow] +# - -# We now move on to the definition of the three variational forms, one for each step in the IPCS scheme. Let us look at the definition of the first variational problem and the relevant parameters. +# We now move on to the definition of the three variational forms, one for each step in the IPCS scheme. +# Let us look at the definition of the first variational problem and the relevant parameters. u_n = Function(V) u_n.name = "u_n" @@ -159,25 +249,34 @@ def outflow(x): rho = Constant(mesh, PETSc.ScalarType(1)) -# ```{admonition} Usage of "dolfinx.Constant" -# Note that we have wrapped several parameters as constants. This is to reduce the compilation-time of the variational formulations. By wrapping them as a constant, we can change the variable +# ```{admonition} Usage of "dolfinx.fem.Constant" +# Note that we have wrapped several parameters as constants. +# This is to reduce the compilation-time of the variational formulations. +# By wrapping them as a constant, we can change the variable # ``` # The next step is to set up the variational form of the first step. -# As the variational problem contains a mix of known and unknown quantities, we will use the following naming convention: `u` (mathematically $u^{n+1}$) is known as a trial function in the variational form. `u_` is the most recently computed approximation ($u^{n+1}$ available as a `Function` object), `u_n` is $u^n$, and the same convention goes for `p,p_` ($p^{n+1}$) and `p_n` (p^n). +# As the variational problem contains a mix of known and unknown quantities, +# we will use the following naming convention: `u` (mathematically $u^{n+1}$) is known as a trial function +# in the variational form. `u_` is the most recently computed approximation +# ($u^{n+1}$ available as a `Function` object), `u_n` is $u^n$, and the same convention +# goes for `p,p_` ($p^{n+1}$) and `p_n` (p^n). + # + -# Define strain-rate tensor def epsilon(u): + """Strain-rate tensor.""" return sym(nabla_grad(u)) -# Define stress tensor - def sigma(u, p): + """Stress tensor.""" return 2 * mu * epsilon(u) - p * Identity(len(u)) +# - + # Define the variational problem for the first step + p_n = Function(Q) p_n.name = "p_n" F1 = rho * dot((u - u_n) / k, v) * dx @@ -187,9 +286,14 @@ def sigma(u, p): F1 -= dot(f, v) * dx a1 = form(lhs(F1)) L1 = form(rhs(F1)) -# - -# Note that we have used the `ufl`-functions `lhs` and `rhs` to sort out the bilinear form $a(u,v)$ and linear form $L(v)$. This is particulary convenient in longer and more complicated variational forms. With our particular discretization $a(u,v)$ `a1` is not time dependent, and only has to be assembled once, while the right hand side is dependent on the solution from the previous time step (`u_n`). Thus, we do as for the [](./heat_code), and create the matrix outside the time-loop. +# Note that we have used the `ufl`-functions `lhs` and `rhs` to sort out the bilinear form +# $a(u,v)$ and linear form $L(v)$. +# This is particulary convenient in longer and more complicated variational forms. +# With our particular discretization $a(u,v)$ `a1` is not time dependent, +# and only has to be assembled once, while the right hand side is dependent on the solution +# from the previous time step (`u_n`). +# Thus, we do as for the [](./heat_code), and create the matrix outside the time-loop. A1 = assemble_matrix(a1, bcs=bcu) A1.assemble() @@ -215,7 +319,13 @@ def sigma(u, p): b3 = create_vector(L3) # - -# As we have create all the linear structures for the problem, we can now create a solver for each of them using PETSc. We can therefore customize the solution strategy for each step. For the tentative velocity step and pressure correction step, we will use the Stabilized version of BiConjugate Gradient to solve the linear system, and using algebraic multigrid for preconditioning. For the last step, the velocity update, we use a conjugate gradient method with successive over relaxation, Gauss Seidel (SOR) preconditioning. +# As we have create all the linear structures for the problem, we can now create a solver for each of them using PETSc. +# We can therefore customize the solution strategy for each step. +# For the tentative velocity step and pressure correction step, +# we will use the Stabilized version of BiConjugate Gradient to solve the linear system, +# and using algebraic multigrid for preconditioning. +# For the last step, the velocity update, we use a conjugate gradient method with successive over relaxation, +# Gauss Seidel (SOR) preconditioning. # + # Solver for step 1 @@ -244,13 +354,16 @@ def sigma(u, p): # We prepare output files for the velocity and pressure data, and write the mesh and initial conditions to file +# + from pathlib import Path + folder = Path("results") folder.mkdir(exist_ok=True, parents=True) vtx_u = VTXWriter(mesh.comm, folder / "poiseuille_u.bp", u_n, engine="BP4") vtx_p = VTXWriter(mesh.comm, folder / "poiseuille_p.bp", p_n, engine="BP4") vtx_u.write(t) vtx_p.write(t) +# - # We also interpolate the analytical solution into our function-space and create a variational formulation for the $L^2$-error. # @@ -313,7 +426,9 @@ def u_exact(x): # Compute error at current time-step error_L2 = np.sqrt(mesh.comm.allreduce(assemble_scalar(L2_error), op=MPI.SUM)) - error_max = mesh.comm.allreduce(np.max(u_.x.petsc_vec.array - u_ex.x.petsc_vec.array), op=MPI.MAX) + error_max = mesh.comm.allreduce( + np.max(u_.x.petsc_vec.array - u_ex.x.petsc_vec.array), op=MPI.MAX + ) # Print error only every 20th step and at the last step if (i % 20 == 0) or (i == num_steps - 1): print(f"Time {t:.2f}, L2-error {error_L2:.2e}, Max error {error_max:.2e}") @@ -328,16 +443,23 @@ def u_exact(x): solver3.destroy() # ## Verification -# As for the previous problems we compute the error at each degree of freedom and the $L^2(\Omega)$-error. We start with the initial condition $u=(0,0)$. We have not specified the initial condition explicitly, and FEniCSx will initialize all `Function`s including `u_n` and `u_` to zero. Since the exact solution is quadratic, we expect to reach machine precision within finite time. For our implementation, we observe that the error quickly approaches zero, and is of order $10^{-6}$ at $T=10$ +# As for the previous problems we compute the error at each degree of freedom and the $L^2(\Omega)$-error. +# We start with the initial condition $u=(0,0)$. +# We have not specified the initial condition explicitly, and FEniCSx will initialize all +# `Function`s including `u_n` and `u_` to zero. +# Since the exact solution is quadratic, we expect to reach machine precision within finite time. +# For our implementation, we observe that the error quickly approaches zero, and is of order $10^{-6}$ at $T=10$ # # ## Visualization of vectors -# We have already looked at how to plot higher order functions and vector functions. In this section we will look at how to visualize vector functions with glyphs, instead of warping the mesh. +# We have already looked at how to plot higher order functions and vector functions. +# In this section we will look at how to visualize vector functions with glyphs, instead of warping the mesh. # + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) + topology, cell_types, geometry = vtk_mesh(V) values = np.zeros((geometry.shape[0], 3), dtype=np.float64) -values[:, :len(u_n)] = u_n.x.array.real.reshape((geometry.shape[0], len(u_n))) +values[:, : len(u_n)] = u_n.x.array.real.reshape((geometry.shape[0], len(u_n))) # Create a point cloud of glyphs function_grid = pyvista.UnstructuredGrid(topology, cell_types, geometry) @@ -345,14 +467,16 @@ def u_exact(x): glyphs = function_grid.glyph(orient="u", factor=0.2) # Create a pyvista-grid for the mesh -mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim) -grid = pyvista.UnstructuredGrid(*vtk_mesh(mesh, mesh.topology.dim)) +tdim = mesh.topology.dim +mesh.topology.create_connectivity(tdim, tdim) +grid = pyvista.UnstructuredGrid(*vtk_mesh(mesh, tdim)) # Create plotter plotter = pyvista.Plotter() plotter.add_mesh(grid, style="wireframe", color="k") plotter.add_mesh(glyphs) plotter.view_xy() + if not pyvista.OFF_SCREEN: plotter.show() else: diff --git a/chapter2/ns_code2.ipynb b/chapter2/ns_code2.ipynb index 312f119d..47a32e88 100644 --- a/chapter2/ns_code2.ipynb +++ b/chapter2/ns_code2.ipynb @@ -9,12 +9,17 @@ "\n", "Author: Jørgen S. Dokken\n", "\n", - "In this section, we will turn our attention to a slightly more challenging problem: flow past a cylinder. The geometry and parameters are taken from the [DFG 2D-3 benchmark](https://wwwold.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark3_re100.html) in FeatFlow.\n", + "In this section, we will turn our attention to a slightly more challenging problem: flow past a cylinder.\n", + "The geometry and parameters are taken from the\n", + "[DFG 2D-3 benchmark](https://wwwold.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark3_re100.html) in FeatFlow.\n", "\n", - "To be able to solve this problem efficiently and ensure numerical stability, we will substitute our first order backward difference scheme with a Crank-Nicholson discretization in time, and a semi-implicit Adams-Bashforth approximation of the non-linear term.\n", + "To be able to solve this problem efficiently and ensure numerical stability,\n", + "we will substitute our first order backward difference scheme with a Crank-Nicholson discretization in time,\n", + "and a semi-implicit Adams-Bashforth approximation of the non-linear term.\n", "\n", "```{admonition} Computationally demanding demo\n", - "This demo is computationally demanding, with a run-time up to 15 minutes, as it is using parameters from the DFG 2D-3 benchmark, which consists of 12800 time steps.\n", + "This demo is computationally demanding, with a run-time up to 15 minutes,\n", + "as it is using parameters from the DFG 2D-3 benchmark, which consists of 12800 time steps.\n", "It is adviced to download this demo and not run it in a browser.\n", "This runtime of the demo can be decreased by using 2 or 3 mpi processes.\n", "```\n", @@ -24,19 +29,18 @@ "\n", "The kinematic velocity is given by $\\nu=0.001=\\frac{\\mu}{\\rho}$ and the inflow velocity profile is specified as\n", "\n", - "$$\n", - " u(x,y,t) = \\left( \\frac{4Uy(0.41-y)}{0.41^2}, 0 \\right)\n", - "$$\n", - "\n", - "$$\n", - " U=U(t) = 1.5\\sin(\\pi t/8)\n", - "$$\n", + "\\begin{align*}\n", + " u(x,y,t) &= \\left( \\frac{4Uy(0.41-y)}{0.41^2}, 0 \\right)\\\\\n", + " U &= U(t) = 1.5\\sin(\\pi t/8)\n", + "\\end{align*}\n", "\n", - "which has a maximum magnitude of $1.5$ at $y=0.41/2$. We do not use any scaling for this problem since all exact parameters are known.\n", + "which has a maximum magnitude of $1.5$ at $y=0.41/2$.\n", + "We do not use any scaling for this problem since all exact parameters are known.\n", "\n", "## Mesh generation\n", "\n", - "As in the [Deflection of a membrane](./../chapter1/membrane_code.ipynb) we use GMSH to generate the mesh. We fist create the rectangle and obstacle.\n" + "As in the [Deflection of a membrane](./../chapter1/membrane_code.ipynb) we use GMSH to generate the mesh.\n", + "We fist create the rectangle and obstacle.\n" ] }, { @@ -112,7 +116,7 @@ "id": "2", "metadata": {}, "source": [ - "The next step is to subtract the obstacle from the channel, such that we do not mesh the interior of the circle.\n" + "The next step is to subtract the obstacle from the channel, such that we do not mesh the interior of the circle." ] }, { @@ -132,7 +136,7 @@ "id": "4", "metadata": {}, "source": [ - "To get GMSH to mesh the fluid, we add a physical volume marker\n" + "To get GMSH to mesh the fluid, we add a physical volume marker" ] }, { @@ -155,7 +159,9 @@ "id": "6", "metadata": {}, "source": [ - "To tag the different surfaces of the mesh, we tag the inflow (left hand side) with marker 2, the outflow (right hand side) with marker 3 and the fluid walls with 4 and obstacle with 5. We will do this by computing the center of mass for each geometrical entity.\n" + "To tag the different surfaces of the mesh, we tag the inflow (left hand side) with marker 2,\n", + "the outflow (right hand side) with marker 3 and the fluid walls with 4 and obstacle with 5.\n", + "We will do this by computing the center of mass for each geometrical entity." ] }, { @@ -196,7 +202,23 @@ "id": "8", "metadata": {}, "source": [ - "In our previous meshes, we have used uniform mesh sizes. In this example, we will have variable mesh sizes to resolve the flow solution in the area of interest; close to the circular obstacle. To do this, we use GMSH Fields.\n" + "In our previous meshes, we have used uniform mesh sizes.\n", + "In this example, we will have variable mesh sizes to resolve the flow solution in the area of interest;\n", + "close to the circular obstacle. To do this, we use GMSH Fields.\n" + ] + }, + { + "cell_type": "markdown", + "id": "ed804169", + "metadata": {}, + "source": [ + "Create distance field from obstacle.\n", + "Add threshold of mesh sizes based on the distance field\n", + "LcMax - /--------\n", + " /\n", + "LcMin -o---------/\n", + " | | |\n", + " Point DistMin DistMax" ] }, { @@ -206,13 +228,6 @@ "metadata": {}, "outputs": [], "source": [ - "# Create distance field from obstacle.\n", - "# Add threshold of mesh sizes based on the distance field\n", - "# LcMax - /--------\n", - "# /\n", - "# LcMin -o---------/\n", - "# | | |\n", - "# Point DistMin DistMax\n", "res_min = r / 3\n", "if mesh_comm.rank == model_rank:\n", " distance_field = gmsh.model.mesh.field.add(\"Distance\")\n", @@ -235,7 +250,9 @@ "source": [ "## Generating the mesh\n", "\n", - "We are now ready to generate the mesh. However, we have to decide if our mesh should consist of triangles or quadrilaterals. In this demo, to match the DFG 2D-3 benchmark, we use second order quadrilateral elements.\n" + "We are now ready to generate the mesh.\n", + "However, we have to decide if our mesh should consist of triangles or quadrilaterals.\n", + "In this demo, to match the DFG 2D-3 benchmark, we use second order quadrilateral elements." ] }, { @@ -263,8 +280,10 @@ "## Loading mesh and boundary markers\n", "\n", "As we have generated the mesh, we now need to load the mesh and corresponding facet markers into DOLFINx.\n", - "To load the mesh, we follow the same structure as in [Deflection of a membrane](./../chapter1/membrane_code.ipynb), with the difference being that we will load in facet markers as well.\n", - "To learn more about the specifics of the function below, see [A GMSH tutorial for DOLFINx](https://jsdokken.com/src/tutorial_gmsh.html).\n" + "To load the mesh, we follow the same structure as in [Deflection of a membrane](./../chapter1/membrane_code.ipynb),\n", + "with the difference being that we will load in facet markers as well.\n", + "To learn more about the specifics of the function below,\n", + "see [A GMSH tutorial for DOLFINx](https://jsdokken.com/src/tutorial_gmsh.html)." ] }, { @@ -288,7 +307,7 @@ "source": [ "## Physical and discretization parameters\n", "\n", - "Following the DGF-2 benchmark, we define our problem specific parameters\n" + "Following the DGF-2 benchmark, we define our problem specific parameters" ] }, { @@ -298,8 +317,8 @@ "metadata": {}, "outputs": [], "source": [ - "t = 0\n", - "T = 8 # Final time\n", + "t = 0.0\n", + "T = 8.0 # Final time\n", "dt = 1 / 1600 # Time step size\n", "num_steps = int(T / dt)\n", "k = Constant(mesh, PETSc.ScalarType(dt))\n", @@ -312,32 +331,50 @@ "id": "16", "metadata": {}, "source": [ - "```{admonition} Reduced end-time of problem\n", - "In the current demo, we have reduced the run time to one second to make it easier to illustrate the concepts of the benchmark. By increasing the end-time `T` to 8, the runtime in a notebook is approximately 25 minutes. If you convert the notebook to a python file and use `mpirun`, you can reduce the runtime of the problem.\n", + "```{admonition} Reduce runtime of problem\n", + "This problem takes about 15 minutes to run in serial, due to the large amount of time steps.\n", + "If you convert the notebook to a python file and use `mpirun`, you can reduce the runtime of the problem.\n", "```\n", "\n", "## Boundary conditions\n", "\n", - "As we have created the mesh and relevant mesh tags, we can now specify the function spaces `V` and `Q` along with the boundary conditions. As the `ft` contains markers for facets, we use this class to find the facets for the inlet and walls.\n" + "As we have created the mesh and relevant mesh tags, we can now specify the\n", + "function spaces `V` and `Q` along with the boundary conditions.\n", + "As the `ft` contains markers for facets, we use this class to find the facets for the inlet and walls.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "800c547b", "metadata": {}, "outputs": [], "source": [ - "v_cg2 = element(\"Lagrange\", mesh.topology.cell_name(), 2, shape=(mesh.geometry.dim,))\n", - "s_cg1 = element(\"Lagrange\", mesh.topology.cell_name(), 1)\n", + "v_cg2 = element(\"Lagrange\", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,))\n", + "s_cg1 = element(\"Lagrange\", mesh.basix_cell(), 1)\n", "V = functionspace(mesh, v_cg2)\n", "Q = functionspace(mesh, s_cg1)\n", "\n", - "fdim = mesh.topology.dim - 1\n", - "\n", - "# Define boundary conditions\n", - "\n", - "\n", + "fdim = mesh.topology.dim - 1" + ] + }, + { + "cell_type": "markdown", + "id": "f2ac12e5", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "Define boundary conditions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ "class InletVelocity:\n", " def __init__(self, t):\n", " self.t = t\n", @@ -381,11 +418,13 @@ "source": [ "## Variational form\n", "\n", - "As opposed to [Pouseille flow](./ns_code1.ipynb), we will use a Crank-Nicolson discretization, and an semi-implicit Adams-Bashforth approximation.\n", - "The first step can be written as\n", + "As opposed to [Pouseille flow](./ns_code1.ipynb), we will use a Crank-Nicolson discretization,\n", + "and an semi-implicit Adams-Bashforth approximation. The first step can be written as\n", "\n", "$$\n", - "\\rho\\left(\\frac{u^*- u^n}{\\delta t} + \\left(\\frac{3}{2}u^{n} - \\frac{1}{2} u^{n-1}\\right)\\cdot \\frac{1}{2}\\nabla (u^*+u^n) \\right) - \\frac{1}{2}\\mu \\Delta( u^*+ u^n )+ \\nabla p^{n-1/2} = f^{n+\\frac{1}{2}} \\qquad \\text{ in } \\Omega\n", + "\\rho\\left(\\frac{u^*- u^n}{\\delta t} + \\left(\\frac{3}{2}u^{n}\n", + "- \\frac{1}{2} u^{n-1}\\right)\\cdot \\frac{1}{2}\\nabla (u^*+u^n) \\right)\n", + "- \\frac{1}{2}\\mu \\Delta( u^*+ u^n )+ \\nabla p^{n-1/2} = f^{n+\\frac{1}{2}} \\qquad \\text{ in } \\Omega\n", "$$\n", "\n", "$$\n", @@ -396,7 +435,9 @@ "\\frac{1}{2}\\nu \\nabla (u^*+u^n) \\cdot n = p^{n-\\frac{1}{2}} \\qquad \\text{ on } \\partial \\Omega_{N}\n", "$$\n", "\n", - "where we have used the two previous time steps in the temporal derivative for the velocity, and compute the pressure staggered in time, at the time between the previous and current solution. The second step becomes\n", + "where we have used the two previous time steps in the temporal derivative for the velocity,\n", + "and compute the pressure staggered in time, at the time between the previous and current solution.\n", + "The second step becomes\n", "\n", "$$\n", "\\nabla^2 \\phi = \\frac{\\rho}{\\delta t} \\nabla \\cdot u^* \\qquad\\text{in } \\Omega,\n", @@ -429,16 +470,14 @@ "source": [ "u = TrialFunction(V)\n", "v = TestFunction(V)\n", - "u_ = Function(V)\n", - "u_.name = \"u\"\n", - "u_s = Function(V)\n", + "u_ = Function(V, name=\"u\")\n", + "u_s = Function(V, name=\"u_tentative\")\n", "u_n = Function(V)\n", "u_n1 = Function(V)\n", "p = TrialFunction(Q)\n", "q = TestFunction(Q)\n", - "p_ = Function(Q)\n", - "p_.name = \"p\"\n", - "phi = Function(Q)" + "p_ = Function(Q, name=\"p\")\n", + "phi = Function(Q, name=\"phi\")" ] }, { @@ -446,7 +485,8 @@ "id": "20", "metadata": {}, "source": [ - "Next, we define the variational formulation for the first step, where we have integrated the diffusion term, as well as the pressure term by parts.\n" + "Next, we define the variational formulation for the first step,\n", + "where we have integrated the diffusion term, as well as the pressure term by parts." ] }, { @@ -472,7 +512,7 @@ "id": "22", "metadata": {}, "source": [ - "Next we define the second step\n" + "Next we define the second step" ] }, { @@ -494,7 +534,7 @@ "id": "24", "metadata": {}, "source": [ - "We finally create the last step\n" + "We finally create the last step" ] }, { @@ -556,17 +596,18 @@ "source": [ "## Verification of the implementation compute known physical quantities\n", "\n", - "As a further verification of our implementation, we compute the drag and lift coefficients over the obstacle, defined as\n", + "As a further verification of our implementation, we compute the drag and lift\n", + "coefficients over the obstacle, defined as\n", "\n", - "$$\n", - " C_{\\text{D}}(u,p,t,\\partial\\Omega_S) = \\frac{2}{\\rho L U_{mean}^2}\\int_{\\partial\\Omega_S}\\rho \\nu n \\cdot \\nabla u_{t_S}(t)n_y -p(t)n_x~\\mathrm{d} s,\n", - "$$\n", - "\n", - "$$\n", - " C_{\\text{L}}(u,p,t,\\partial\\Omega_S) = -\\frac{2}{\\rho L U_{mean}^2}\\int_{\\partial\\Omega_S}\\rho \\nu n \\cdot \\nabla u_{t_S}(t)n_x + p(t)n_y~\\mathrm{d} s,\n", - "$$\n", + "\\begin{align*}\n", + " C_{\\text{D}}(u,p,t,\\partial\\Omega_S) &=\n", + "\\frac{2}{\\rho L U_{mean}^2}\\int_{\\partial\\Omega_S}\\rho \\nu n \\cdot \\nabla u_{t_S}(t)n_y -p(t)n_x~\\mathrm{d} s,\\\\\n", + " C_{\\text{L}}(u,p,t,\\partial\\Omega_S) &= -\\frac{2}{\\rho L U_{mean}^2}\\int_{\\partial\\Omega_S}\\rho \\nu n \\cdot \\nabla u_{t_S}(t)n_x + p(t)n_y~\\mathrm{d} s,\n", + "\\end{align*}\n", "\n", - "where $u_{t_S}$ is the tangential velocity component at the interface of the obstacle $\\partial\\Omega_S$, defined as $u_{t_S}=u\\cdot (n_y,-n_x)$, $U_{mean}=1$ the average inflow velocity, and $L$ the length of the channel. We use `UFL` to create the relevant integrals, and assemble them at each time step.\n" + "where $u_{t_S}$ is the tangential velocity component at the interface of the obstacle $\\partial\\Omega_S$,\n", + "defined as $u_{t_S}=u\\cdot (n_y,-n_x)$, $U_{mean}=1$ the average inflow velocity, and $L$ the length of the channel.\n", + "We use `UFL` to create the relevant integrals, and assemble them at each time step." ] }, { @@ -593,7 +634,10 @@ "id": "30", "metadata": {}, "source": [ - "We will also evaluate the pressure at two points, one in front of the obstacle, $(0.15, 0.2)$, and one behind the obstacle, $(0.25, 0.2)$. To do this, we have to find which cell contains each of the points, so that we can create a linear combination of the local basis functions and coefficients.\n" + "We will also evaluate the pressure at two points, one in front of the obstacle, $(0.15, 0.2)$,\n", + "and one behind the obstacle, $(0.25, 0.2)$.\n", + "To do this, we have to find which cell contains each of the points,\n", + "so that we can create a linear combination of the local basis functions and coefficients." ] }, { @@ -621,11 +665,16 @@ "## Solving the time-dependent problem\n", "\n", "```{admonition} Stability of the Navier-Stokes equation\n", - "Note that the current splitting scheme has to fullfil the a [Courant–Friedrichs–Lewy condition](https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition). This limits the spatial discretization with respect to the inlet velocity and temporal discretization.\n", - "Other temporal discretization schemes such as the second order backward difference discretization or Crank-Nicholson discretization with Adams-Bashforth linearization are better behaved than our simple backward difference scheme.\n", + "Note that the current splitting scheme has to fullfil the a\n", + "[Courant–Friedrichs–Lewy condition](https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition).\n", + "This limits the spatial discretization with respect to the inlet velocity and temporal discretization.\n", + "Other temporal discretization schemes such as the second order backward difference discretization or Crank-Nicholson\n", + "discretization with Adams-Bashforth linearization are better behaved than our simple backward difference scheme.\n", "```\n", "\n", - "As in the previous example, we create output files for the velocity and pressure and solve the time-dependent problem. As we are solving a time dependent problem with many time steps, we use the `tqdm`-package to visualize the progress. This package can be installed with `pip3`.\n" + "As in the previous example, we create output files for the velocity and pressure and solve the time-dependent problem.\n", + "As we are solving a time dependent problem with many time steps, we use the `tqdm`-package to visualize the progress.\n", + "This package can be installed with `pip`." ] }, { @@ -635,16 +684,8 @@ "metadata": {}, "outputs": [], "source": [ - "from pathlib import Path" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "34", - "metadata": {}, - "outputs": [], - "source": [ + "from pathlib import Path\n", + "\n", "folder = Path(\"results\")\n", "folder.mkdir(exist_ok=True, parents=True)\n", "vtx_u = VTXWriter(mesh.comm, folder / \"dfg2D-3-u.bp\", [u_], engine=\"BP4\")\n", @@ -739,6 +780,32 @@ "vtx_p.close()" ] }, + { + "cell_type": "markdown", + "id": "779e8f68", + "metadata": {}, + "source": [ + "Destroy PETSc objects to free memory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "A1.destroy()\n", + "A2.destroy()\n", + "A3.destroy()\n", + "b1.destroy()\n", + "b2.destroy()\n", + "b3.destroy()\n", + "solver1.destroy()\n", + "solver2.destroy()\n", + "solver3.destroy()" + ] + }, { "cell_type": "markdown", "id": "35", @@ -746,7 +813,8 @@ "source": [ "## Verification using data from FEATFLOW\n", "\n", - "As FEATFLOW has provided data for different discretization levels, we compare our numerical data with the data provided using `matplotlib`.\n" + "As FEATFLOW has provided data for different discretization levels,\n", + "we compare our numerical data with the data provided using `matplotlib`." ] }, { diff --git a/chapter2/ns_code2.py b/chapter2/ns_code2.py index 20795db3..68e4f92f 100644 --- a/chapter2/ns_code2.py +++ b/chapter2/ns_code2.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.6 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -17,12 +17,17 @@ # # Author: Jørgen S. Dokken # -# In this section, we will turn our attention to a slightly more challenging problem: flow past a cylinder. The geometry and parameters are taken from the [DFG 2D-3 benchmark](https://wwwold.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark3_re100.html) in FeatFlow. +# In this section, we will turn our attention to a slightly more challenging problem: flow past a cylinder. +# The geometry and parameters are taken from the +# [DFG 2D-3 benchmark](https://wwwold.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark3_re100.html) in FeatFlow. # -# To be able to solve this problem efficiently and ensure numerical stability, we will substitute our first order backward difference scheme with a Crank-Nicholson discretization in time, and a semi-implicit Adams-Bashforth approximation of the non-linear term. +# To be able to solve this problem efficiently and ensure numerical stability, +# we will substitute our first order backward difference scheme with a Crank-Nicholson discretization in time, +# and a semi-implicit Adams-Bashforth approximation of the non-linear term. # # ```{admonition} Computationally demanding demo -# This demo is computationally demanding, with a run-time up to 15 minutes, as it is using parameters from the DFG 2D-3 benchmark, which consists of 12800 time steps. +# This demo is computationally demanding, with a run-time up to 15 minutes, +# as it is using parameters from the DFG 2D-3 benchmark, which consists of 12800 time steps. # It is adviced to download this demo and not run it in a browser. # This runtime of the demo can be decreased by using 2 or 3 mpi processes. # ``` @@ -32,19 +37,18 @@ # # The kinematic velocity is given by $\nu=0.001=\frac{\mu}{\rho}$ and the inflow velocity profile is specified as # -# $$ -# u(x,y,t) = \left( \frac{4Uy(0.41-y)}{0.41^2}, 0 \right) -# $$ -# -# $$ -# U=U(t) = 1.5\sin(\pi t/8) -# $$ +# \begin{align*} +# u(x,y,t) &= \left( \frac{4Uy(0.41-y)}{0.41^2}, 0 \right)\\ +# U &= U(t) = 1.5\sin(\pi t/8) +# \end{align*} # -# which has a maximum magnitude of $1.5$ at $y=0.41/2$. We do not use any scaling for this problem since all exact parameters are known. +# which has a maximum magnitude of $1.5$ at $y=0.41/2$. +# We do not use any scaling for this problem since all exact parameters are known. # # ## Mesh generation # -# As in the [Deflection of a membrane](./../chapter1/membrane_code.ipynb) we use GMSH to generate the mesh. We fist create the rectangle and obstacle. +# As in the [Deflection of a membrane](./../chapter1/membrane_code.ipynb) we use GMSH to generate the mesh. +# We fist create the rectangle and obstacle. # # + @@ -110,14 +114,12 @@ # - # The next step is to subtract the obstacle from the channel, such that we do not mesh the interior of the circle. -# if mesh_comm.rank == model_rank: fluid = gmsh.model.occ.cut([(gdim, rectangle)], [(gdim, obstacle)]) gmsh.model.occ.synchronize() # To get GMSH to mesh the fluid, we add a physical volume marker -# fluid_marker = 1 if mesh_comm.rank == model_rank: @@ -126,8 +128,9 @@ gmsh.model.addPhysicalGroup(volumes[0][0], [volumes[0][1]], fluid_marker) gmsh.model.setPhysicalName(volumes[0][0], fluid_marker, "Fluid") -# To tag the different surfaces of the mesh, we tag the inflow (left hand side) with marker 2, the outflow (right hand side) with marker 3 and the fluid walls with 4 and obstacle with 5. We will do this by computing the center of mass for each geometrical entity. -# +# To tag the different surfaces of the mesh, we tag the inflow (left hand side) with marker 2, +# the outflow (right hand side) with marker 3 and the fluid walls with 4 and obstacle with 5. +# We will do this by computing the center of mass for each geometrical entity. inlet_marker, outlet_marker, wall_marker, obstacle_marker = 2, 3, 4, 5 inflow, outflow, walls, obstacle = [], [], [], [] @@ -154,7 +157,9 @@ gmsh.model.addPhysicalGroup(1, obstacle, obstacle_marker) gmsh.model.setPhysicalName(1, obstacle_marker, "Obstacle") -# In our previous meshes, we have used uniform mesh sizes. In this example, we will have variable mesh sizes to resolve the flow solution in the area of interest; close to the circular obstacle. To do this, we use GMSH Fields. +# In our previous meshes, we have used uniform mesh sizes. +# In this example, we will have variable mesh sizes to resolve the flow solution in the area of interest; +# close to the circular obstacle. To do this, we use GMSH Fields. # # Create distance field from obstacle. @@ -164,6 +169,7 @@ # LcMin -o---------/ # | | | # Point DistMin DistMax + res_min = r / 3 if mesh_comm.rank == model_rank: distance_field = gmsh.model.mesh.field.add("Distance") @@ -180,8 +186,9 @@ # ## Generating the mesh # -# We are now ready to generate the mesh. However, we have to decide if our mesh should consist of triangles or quadrilaterals. In this demo, to match the DFG 2D-3 benchmark, we use second order quadrilateral elements. -# +# We are now ready to generate the mesh. +# However, we have to decide if our mesh should consist of triangles or quadrilaterals. +# In this demo, to match the DFG 2D-3 benchmark, we use second order quadrilateral elements. if mesh_comm.rank == model_rank: gmsh.option.setNumber("Mesh.Algorithm", 8) @@ -195,9 +202,10 @@ # ## Loading mesh and boundary markers # # As we have generated the mesh, we now need to load the mesh and corresponding facet markers into DOLFINx. -# To load the mesh, we follow the same structure as in [Deflection of a membrane](./../chapter1/membrane_code.ipynb), with the difference being that we will load in facet markers as well. -# To learn more about the specifics of the function below, see [A GMSH tutorial for DOLFINx](https://jsdokken.com/src/tutorial_gmsh.html). -# +# To load the mesh, we follow the same structure as in [Deflection of a membrane](./../chapter1/membrane_code.ipynb), +# with the difference being that we will load in facet markers as well. +# To learn more about the specifics of the function below, +# see [A GMSH tutorial for DOLFINx](https://jsdokken.com/src/tutorial_gmsh.html). mesh_data = gmshio.model_to_mesh(gmsh.model, mesh_comm, model_rank, gdim=gdim) mesh = mesh_data.mesh @@ -208,36 +216,40 @@ # ## Physical and discretization parameters # # Following the DGF-2 benchmark, we define our problem specific parameters -# -t = 0 -T = 8 # Final time +t = 0.0 +T = 8.0 # Final time dt = 1 / 1600 # Time step size num_steps = int(T / dt) k = Constant(mesh, PETSc.ScalarType(dt)) mu = Constant(mesh, PETSc.ScalarType(0.001)) # Dynamic viscosity rho = Constant(mesh, PETSc.ScalarType(1)) # Density -# ```{admonition} Reduced end-time of problem -# In the current demo, we have reduced the run time to one second to make it easier to illustrate the concepts of the benchmark. By increasing the end-time `T` to 8, the runtime in a notebook is approximately 25 minutes. If you convert the notebook to a python file and use `mpirun`, you can reduce the runtime of the problem. +# ```{admonition} Reduce runtime of problem +# This problem takes about 15 minutes to run in serial, due to the large amount of time steps. +# If you convert the notebook to a python file and use `mpirun`, you can reduce the runtime of the problem. # ``` # # ## Boundary conditions # -# As we have created the mesh and relevant mesh tags, we can now specify the function spaces `V` and `Q` along with the boundary conditions. As the `ft` contains markers for facets, we use this class to find the facets for the inlet and walls. +# As we have created the mesh and relevant mesh tags, we can now specify the +# function spaces `V` and `Q` along with the boundary conditions. +# As the `ft` contains markers for facets, we use this class to find the facets for the inlet and walls. # # + -v_cg2 = element("Lagrange", mesh.topology.cell_name(), 2, shape=(mesh.geometry.dim,)) -s_cg1 = element("Lagrange", mesh.topology.cell_name(), 1) +v_cg2 = element("Lagrange", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,)) +s_cg1 = element("Lagrange", mesh.basix_cell(), 1) V = functionspace(mesh, v_cg2) Q = functionspace(mesh, s_cg1) fdim = mesh.topology.dim - 1 +# - # Define boundary conditions +# + class InletVelocity: def __init__(self, t): self.t = t @@ -276,11 +288,13 @@ def __call__(self, x): # ## Variational form # -# As opposed to [Pouseille flow](./ns_code1.ipynb), we will use a Crank-Nicolson discretization, and an semi-implicit Adams-Bashforth approximation. -# The first step can be written as +# As opposed to [Pouseille flow](./ns_code1.ipynb), we will use a Crank-Nicolson discretization, +# and an semi-implicit Adams-Bashforth approximation. The first step can be written as # # $$ -# \rho\left(\frac{u^*- u^n}{\delta t} + \left(\frac{3}{2}u^{n} - \frac{1}{2} u^{n-1}\right)\cdot \frac{1}{2}\nabla (u^*+u^n) \right) - \frac{1}{2}\mu \Delta( u^*+ u^n )+ \nabla p^{n-1/2} = f^{n+\frac{1}{2}} \qquad \text{ in } \Omega +# \rho\left(\frac{u^*- u^n}{\delta t} + \left(\frac{3}{2}u^{n} +# - \frac{1}{2} u^{n-1}\right)\cdot \frac{1}{2}\nabla (u^*+u^n) \right) +# - \frac{1}{2}\mu \Delta( u^*+ u^n )+ \nabla p^{n-1/2} = f^{n+\frac{1}{2}} \qquad \text{ in } \Omega # $$ # # $$ @@ -291,7 +305,9 @@ def __call__(self, x): # \frac{1}{2}\nu \nabla (u^*+u^n) \cdot n = p^{n-\frac{1}{2}} \qquad \text{ on } \partial \Omega_{N} # $$ # -# where we have used the two previous time steps in the temporal derivative for the velocity, and compute the pressure staggered in time, at the time between the previous and current solution. The second step becomes +# where we have used the two previous time steps in the temporal derivative for the velocity, +# and compute the pressure staggered in time, at the time between the previous and current solution. +# The second step becomes # # $$ # \nabla^2 \phi = \frac{\rho}{\delta t} \nabla \cdot u^* \qquad\text{in } \Omega, @@ -317,19 +333,17 @@ def __call__(self, x): u = TrialFunction(V) v = TestFunction(V) -u_ = Function(V) -u_.name = "u" -u_s = Function(V) +u_ = Function(V, name="u") +u_s = Function(V, name="u_tentative") u_n = Function(V) u_n1 = Function(V) p = TrialFunction(Q) q = TestFunction(Q) -p_ = Function(Q) -p_.name = "p" -phi = Function(Q) +p_ = Function(Q, name="p") +phi = Function(Q, name="phi") -# Next, we define the variational formulation for the first step, where we have integrated the diffusion term, as well as the pressure term by parts. -# +# Next, we define the variational formulation for the first step, +# where we have integrated the diffusion term, as well as the pressure term by parts. f = Constant(mesh, PETSc.ScalarType((0, 0))) F1 = rho / k * dot(u - u_n, v) * dx @@ -342,7 +356,6 @@ def __call__(self, x): b1 = create_vector(L1) # Next we define the second step -# a2 = form(dot(grad(p), grad(q)) * dx) L2 = form(-rho / k * dot(div(u_s), q) * dx) @@ -351,7 +364,6 @@ def __call__(self, x): b2 = create_vector(L2) # We finally create the last step -# a3 = form(rho * dot(u, v) * dx) L3 = form(rho * dot(u_s, v) * dx - k * dot(nabla_grad(phi), v) * dx) @@ -388,18 +400,18 @@ def __call__(self, x): # ## Verification of the implementation compute known physical quantities # -# As a further verification of our implementation, we compute the drag and lift coefficients over the obstacle, defined as -# -# $$ -# C_{\text{D}}(u,p,t,\partial\Omega_S) = \frac{2}{\rho L U_{mean}^2}\int_{\partial\Omega_S}\rho \nu n \cdot \nabla u_{t_S}(t)n_y -p(t)n_x~\mathrm{d} s, -# $$ -# -# $$ -# C_{\text{L}}(u,p,t,\partial\Omega_S) = -\frac{2}{\rho L U_{mean}^2}\int_{\partial\Omega_S}\rho \nu n \cdot \nabla u_{t_S}(t)n_x + p(t)n_y~\mathrm{d} s, -# $$ +# As a further verification of our implementation, we compute the drag and lift +# coefficients over the obstacle, defined as # -# where $u_{t_S}$ is the tangential velocity component at the interface of the obstacle $\partial\Omega_S$, defined as $u_{t_S}=u\cdot (n_y,-n_x)$, $U_{mean}=1$ the average inflow velocity, and $L$ the length of the channel. We use `UFL` to create the relevant integrals, and assemble them at each time step. +# \begin{align*} +# C_{\text{D}}(u,p,t,\partial\Omega_S) &= +# \frac{2}{\rho L U_{mean}^2}\int_{\partial\Omega_S}\rho \nu n \cdot \nabla u_{t_S}(t)n_y -p(t)n_x~\mathrm{d} s,\\ +# C_{\text{L}}(u,p,t,\partial\Omega_S) &= -\frac{2}{\rho L U_{mean}^2}\int_{\partial\Omega_S}\rho \nu n \cdot \nabla u_{t_S}(t)n_x + p(t)n_y~\mathrm{d} s, +# \end{align*} # +# where $u_{t_S}$ is the tangential velocity component at the interface of the obstacle $\partial\Omega_S$, +# defined as $u_{t_S}=u\cdot (n_y,-n_x)$, $U_{mean}=1$ the average inflow velocity, and $L$ the length of the channel. +# We use `UFL` to create the relevant integrals, and assemble them at each time step. n = -FacetNormal(mesh) # Normal pointing out of obstacle dObs = Measure("ds", domain=mesh, subdomain_data=ft, subdomain_id=obstacle_marker) @@ -412,8 +424,10 @@ def __call__(self, x): t_u = np.zeros(num_steps, dtype=np.float64) t_p = np.zeros(num_steps, dtype=np.float64) -# We will also evaluate the pressure at two points, one in front of the obstacle, $(0.15, 0.2)$, and one behind the obstacle, $(0.25, 0.2)$. To do this, we have to find which cell contains each of the points, so that we can create a linear combination of the local basis functions and coefficients. -# +# We will also evaluate the pressure at two points, one in front of the obstacle, $(0.15, 0.2)$, +# and one behind the obstacle, $(0.25, 0.2)$. +# To do this, we have to find which cell contains each of the points, +# so that we can create a linear combination of the local basis functions and coefficients. tree = bb_tree(mesh, mesh.geometry.dim) points = np.array([[0.15, 0.2, 0], [0.25, 0.2, 0]]) @@ -427,13 +441,18 @@ def __call__(self, x): # ## Solving the time-dependent problem # # ```{admonition} Stability of the Navier-Stokes equation -# Note that the current splitting scheme has to fullfil the a [Courant–Friedrichs–Lewy condition](https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition). This limits the spatial discretization with respect to the inlet velocity and temporal discretization. -# Other temporal discretization schemes such as the second order backward difference discretization or Crank-Nicholson discretization with Adams-Bashforth linearization are better behaved than our simple backward difference scheme. +# Note that the current splitting scheme has to fullfil the a +# [Courant–Friedrichs–Lewy condition](https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition). +# This limits the spatial discretization with respect to the inlet velocity and temporal discretization. +# Other temporal discretization schemes such as the second order backward difference discretization or Crank-Nicholson +# discretization with Adams-Bashforth linearization are better behaved than our simple backward difference scheme. # ``` # -# As in the previous example, we create output files for the velocity and pressure and solve the time-dependent problem. As we are solving a time dependent problem with many time steps, we use the `tqdm`-package to visualize the progress. This package can be installed with `pip3`. -# +# As in the previous example, we create output files for the velocity and pressure and solve the time-dependent problem. +# As we are solving a time dependent problem with many time steps, we use the `tqdm`-package to visualize the progress. +# This package can be installed with `pip`. +# + from pathlib import Path folder = Path("results") @@ -528,11 +547,24 @@ def __call__(self, x): progress.close() vtx_u.close() vtx_p.close() +# - + +# Destroy PETSc objects to free memory + +A1.destroy() +A2.destroy() +A3.destroy() +b1.destroy() +b2.destroy() +b3.destroy() +solver1.destroy() +solver2.destroy() +solver3.destroy() # ## Verification using data from FEATFLOW # -# As FEATFLOW has provided data for different discretization levels, we compare our numerical data with the data provided using `matplotlib`. -# +# As FEATFLOW has provided data for different discretization levels, +# we compare our numerical data with the data provided using `matplotlib`. if mesh.comm.rank == 0: if not os.path.exists("figures"): diff --git a/chapter3/component_bc.ipynb b/chapter3/component_bc.ipynb index a6cd7167..9c592eea 100644 --- a/chapter3/component_bc.ipynb +++ b/chapter3/component_bc.ipynb @@ -47,12 +47,29 @@ "import pyvista\n", "import numpy as np\n", "from mpi4py import MPI\n", - "from ufl import Identity, Measure, TestFunction, TrialFunction, dot, dx, inner, grad, nabla_div, sym\n", + "from ufl import (\n", + " Identity,\n", + " Measure,\n", + " TestFunction,\n", + " TrialFunction,\n", + " dot,\n", + " dx,\n", + " inner,\n", + " grad,\n", + " nabla_div,\n", + " sym,\n", + ")\n", "from dolfinx import default_scalar_type\n", "from dolfinx.mesh import CellType, create_rectangle, locate_entities_boundary\n", "from dolfinx.fem.petsc import LinearProblem\n", - "from dolfinx.fem import (Constant, dirichletbc, Function, functionspace, locate_dofs_geometrical,\n", - " locate_dofs_topological)\n", + "from dolfinx.fem import (\n", + " Constant,\n", + " dirichletbc,\n", + " Function,\n", + " functionspace,\n", + " locate_dofs_geometrical,\n", + " locate_dofs_topological,\n", + ")\n", "from dolfinx.plot import vtk_mesh\n", "\n", "L = 1\n", @@ -74,17 +91,22 @@ "cell_type": "code", "execution_count": null, "metadata": { + "lines_to_next_cell": 2, "tags": [] }, "outputs": [], "source": [ - "mesh = create_rectangle(MPI.COMM_WORLD, np.array([[0, 0], [L, H]]), [30, 30], cell_type=CellType.triangle)\n", - "V = functionspace(mesh, (\"Lagrange\", 1, (mesh.geometry.dim , )))" + "mesh = create_rectangle(\n", + " MPI.COMM_WORLD, np.array([[0, 0], [L, H]]), [30, 30], cell_type=CellType.triangle\n", + ")\n", + "V = functionspace(mesh, (\"Lagrange\", 1, (mesh.geometry.dim,)))" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "## Boundary conditions\n", "As we would like to clamp the boundary at $x=0$, we do this by using a marker function, we use `dolfinx.fem.locate_dofs_geometrical` to identify the relevant degrees of freedom." @@ -116,21 +138,11 @@ }, { "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, we locate the degrees of freedom on the top boundary. However, as the boundary condition is in a sub space of our solution, we need to supply both the parent space $V$ and the sub space $V_0$ to `dolfinx.locate_dofs_topological`." - ] - }, - { - "cell_type": "code", - "execution_count": null, "metadata": { - "tags": [] + "lines_to_next_cell": 2 }, - "outputs": [], "source": [ - "def right(x):\n", - " return np.logical_and(np.isclose(x[0], L), x[1] < H)" + "Next, we locate the degrees of freedom on the top boundary. However, as the boundary condition is in a sub space of our solution, we need to supply both the parent space $V$ and the sub space $V_0$ to `dolfinx.locate_dofs_topological`." ] }, { @@ -141,8 +153,14 @@ }, "outputs": [], "source": [ + "def right(x):\n", + " return np.logical_and(np.isclose(x[0], L), x[1] < H)\n", + "\n", + "\n", "boundary_facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, right)\n", - "boundary_dofs_x = locate_dofs_topological(V.sub(0), mesh.topology.dim - 1, boundary_facets)" + "boundary_dofs_x = locate_dofs_topological(\n", + " V.sub(0), mesh.topology.dim - 1, boundary_facets\n", + ")" ] }, { @@ -168,7 +186,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.Constant`" + "As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.fem.Constant`" ] }, { @@ -193,6 +211,7 @@ "cell_type": "code", "execution_count": null, "metadata": { + "lines_to_next_cell": 2, "tags": [] }, "outputs": [], @@ -202,7 +221,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "## Variational formulation\n", "We are now ready to create our variational formulation in close to mathematical syntax, as for the previous problems." @@ -247,7 +268,13 @@ }, "outputs": [], "source": [ - "problem = LinearProblem(a, L, bcs=bcs, petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"component_bc_\",\n", + ")\n", "uh = problem.solve()" ] }, @@ -266,7 +293,7 @@ }, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "\n", "# Create plotter and pyvista grid\n", "p = pyvista.Plotter()\n", @@ -276,7 +303,7 @@ "# Attach vector values to grid and warp grid by vector\n", "\n", "vals = np.zeros((x.shape[0], 3))\n", - "vals[:, :len(uh)] = uh.x.array.reshape((x.shape[0], len(uh)))\n", + "vals[:, : len(uh)] = uh.x.array.reshape((x.shape[0], len(uh)))\n", "grid[\"u\"] = vals\n", "actor_0 = p.add_mesh(grid, style=\"wireframe\", color=\"k\")\n", "warped = grid.warp_by_vector(\"u\", factor=1.5)\n", diff --git a/chapter3/component_bc.py b/chapter3/component_bc.py index 0bd1afff..321157be 100644 --- a/chapter3/component_bc.py +++ b/chapter3/component_bc.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -48,12 +48,29 @@ import pyvista import numpy as np from mpi4py import MPI -from ufl import Identity, Measure, TestFunction, TrialFunction, dot, dx, inner, grad, nabla_div, sym +from ufl import ( + Identity, + Measure, + TestFunction, + TrialFunction, + dot, + dx, + inner, + grad, + nabla_div, + sym, +) from dolfinx import default_scalar_type from dolfinx.mesh import CellType, create_rectangle, locate_entities_boundary from dolfinx.fem.petsc import LinearProblem -from dolfinx.fem import (Constant, dirichletbc, Function, functionspace, locate_dofs_geometrical, - locate_dofs_topological) +from dolfinx.fem import ( + Constant, + dirichletbc, + Function, + functionspace, + locate_dofs_geometrical, + locate_dofs_topological, +) from dolfinx.plot import vtk_mesh L = 1 @@ -66,13 +83,16 @@ # As in the previous demos, we define our mesh and function space. -mesh = create_rectangle(MPI.COMM_WORLD, np.array([[0, 0], [L, H]]), [30, 30], cell_type=CellType.triangle) -V = functionspace(mesh, ("Lagrange", 1, (mesh.geometry.dim , ))) +mesh = create_rectangle( + MPI.COMM_WORLD, np.array([[0, 0], [L, H]]), [30, 30], cell_type=CellType.triangle +) +V = functionspace(mesh, ("Lagrange", 1, (mesh.geometry.dim,))) # ## Boundary conditions # As we would like to clamp the boundary at $x=0$, we do this by using a marker function, we use `dolfinx.fem.locate_dofs_geometrical` to identify the relevant degrees of freedom. + # + def clamped_boundary(x): return np.isclose(x[1], 0) @@ -80,8 +100,6 @@ def clamped_boundary(x): u_zero = np.array((0,) * mesh.geometry.dim, dtype=default_scalar_type) bc = dirichletbc(u_zero, locate_dofs_geometrical(V, clamped_boundary), V) - - # - # Next we would like to constrain the $x$-component of our solution at $x=L$ to $0$. We start by creating the sub space only containing the $x$ @@ -89,19 +107,24 @@ def clamped_boundary(x): # Next, we locate the degrees of freedom on the top boundary. However, as the boundary condition is in a sub space of our solution, we need to supply both the parent space $V$ and the sub space $V_0$ to `dolfinx.locate_dofs_topological`. + +# + def right(x): return np.logical_and(np.isclose(x[0], L), x[1] < H) boundary_facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, right) -boundary_dofs_x = locate_dofs_topological(V.sub(0), mesh.topology.dim - 1, boundary_facets) +boundary_dofs_x = locate_dofs_topological( + V.sub(0), mesh.topology.dim - 1, boundary_facets +) +# - # We can now create our Dirichlet condition bcx = dirichletbc(default_scalar_type(0), boundary_dofs_x, V.sub(0)) bcs = [bc, bcx] -# As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.Constant` +# As we want the traction $T$ over the remaining boundary to be $0$, we create a `dolfinx.fem.Constant` T = Constant(mesh, default_scalar_type((0, 0))) @@ -113,6 +136,7 @@ def right(x): # ## Variational formulation # We are now ready to create our variational formulation in close to mathematical syntax, as for the previous problems. + # + def epsilon(u): return sym(grad(u)) @@ -132,13 +156,19 @@ def sigma(u): # ## Solve the linear variational problem # As in the previous demos, we assemble the matrix and right hand side vector and use PETSc to solve our variational problem -problem = LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) +problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="component_bc_", +) uh = problem.solve() # ## Visualization # + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) # Create plotter and pyvista grid p = pyvista.Plotter() @@ -148,7 +178,7 @@ def sigma(u): # Attach vector values to grid and warp grid by vector vals = np.zeros((x.shape[0], 3)) -vals[:, :len(uh)] = uh.x.array.reshape((x.shape[0], len(uh))) +vals[:, : len(uh)] = uh.x.array.reshape((x.shape[0], len(uh))) grid["u"] = vals actor_0 = p.add_mesh(grid, style="wireframe", color="k") warped = grid.warp_by_vector("u", factor=1.5) diff --git a/chapter3/em.ipynb b/chapter3/em.ipynb index 64536f37..95bcceb7 100644 --- a/chapter3/em.ipynb +++ b/chapter3/em.ipynb @@ -17,7 +17,9 @@ "Through the copper wires a static current of $J=1A$ is flowing.\n", "We would like to compute the magnetic field $B$ in the iron cylinder, the copper wires, and the surrounding vaccum.\n", "\n", - "We start by simplifying the problem to a 2D problem. We can do this by assuming that the cylinder extends far along the z-axis and as a consequence the field is virtually independent of the z-coordinate.\n", + "We start by simplifying the problem to a 2D problem.\n", + "We can do this by assuming that the cylinder extends far along the z-axis and\n", + "as a consequence the field is virtually independent of the z-coordinate.\n", "Next, we consider Maxwell's equation to derive a Poisson equation for the magnetic field (or rather its potential)\n", "\n", "$$\n", @@ -93,7 +95,13 @@ "outputs": [], "source": [ "from dolfinx import default_scalar_type\n", - "from dolfinx.fem import (dirichletbc, Expression, Function, functionspace, locate_dofs_topological)\n", + "from dolfinx.fem import (\n", + " dirichletbc,\n", + " Expression,\n", + " Function,\n", + " functionspace,\n", + " locate_dofs_topological,\n", + ")\n", "from dolfinx.fem.petsc import LinearProblem\n", "from dolfinx.io import XDMFFile\n", "from dolfinx.io.gmshio import model_to_mesh\n", @@ -110,18 +118,17 @@ "rank = MPI.COMM_WORLD.rank\n", "\n", "gmsh.initialize()\n", - "r = 0.1 # Radius of copper wires\n", - "R = 5 # Radius of domain\n", - "a = 1 # Radius of inner iron cylinder\n", - "b = 1.2 # Radius of outer iron cylinder\n", - "N = 8 # Number of windings\n", + "r = 0.1 # Radius of copper wires\n", + "R = 5 # Radius of domain\n", + "a = 1 # Radius of inner iron cylinder\n", + "b = 1.2 # Radius of outer iron cylinder\n", + "N = 8 # Number of windings\n", "c_1 = 0.8 # Radius of inner copper wires\n", "c_2 = 1.4 # Radius of outer copper wires\n", "gdim = 2 # Geometric dimension of the mesh\n", "model_rank = 0\n", "mesh_comm = MPI.COMM_WORLD\n", "if mesh_comm.rank == model_rank:\n", - "\n", " # Define geometry for iron cylinder\n", " outer_iron = gmsh.model.occ.addCircle(0, 0, 0, b)\n", " inner_iron = gmsh.model.occ.addCircle(0, 0, 0, a)\n", @@ -136,11 +143,17 @@ "\n", " # Define the copper-wires inside iron cylinder\n", " angles_N = [i * 2 * np.pi / N for i in range(N)]\n", - " wires_N = [(2, gmsh.model.occ.addDisk(c_1 * np.cos(v), c_1 * np.sin(v), 0, r, r)) for v in angles_N]\n", + " wires_N = [\n", + " (2, gmsh.model.occ.addDisk(c_1 * np.cos(v), c_1 * np.sin(v), 0, r, r))\n", + " for v in angles_N\n", + " ]\n", "\n", " # Define the copper-wires outside the iron cylinder\n", " angles_S = [(i + 0.5) * 2 * np.pi / N for i in range(N)]\n", - " wires_S = [(2, gmsh.model.occ.addDisk(c_2 * np.cos(v), c_2 * np.sin(v), 0, r, r)) for v in angles_S]\n", + " wires_S = [\n", + " (2, gmsh.model.occ.addDisk(c_2 * np.cos(v), c_2 * np.sin(v), 0, r, r))\n", + " for v in angles_S\n", + " ]\n", " gmsh.model.occ.synchronize()\n", " # Resolve all boundaries of the different wires in the background domain\n", " all_surfaces = [(2, iron)]\n", @@ -259,7 +272,7 @@ }, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "plotter = pyvista.Plotter()\n", "tdim = mesh.topology.dim\n", "mesh.topology.create_connectivity(tdim, tdim)\n", @@ -358,7 +371,7 @@ "outputs": [], "source": [ "A_z = Function(V)\n", - "problem = LinearProblem(a, L, u=A_z, bcs=[bc])\n", + "problem = LinearProblem(a, L, u=A_z, bcs=[bc], petsc_options_prefix=\"em_\")\n", "problem.solve()" ] }, @@ -377,7 +390,7 @@ }, "outputs": [], "source": [ - "W = functionspace(mesh, (\"DG\", 0, (mesh.geometry.dim, )))\n", + "W = functionspace(mesh, (\"DG\", 0, (mesh.geometry.dim,)))\n", "B = Function(W)\n", "B_expr = Expression(as_vector((A_z.dx(1), -A_z.dx(0))), W.element.interpolation_points)\n", "B.interpolate(B_expr)" @@ -428,7 +441,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "lines_to_next_cell": 0, "tags": [] }, "outputs": [], @@ -440,12 +452,14 @@ "top_imap = mesh.topology.index_map(mesh.topology.dim)\n", "num_cells = top_imap.size_local + top_imap.num_ghosts\n", "mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim)\n", - "midpoints = compute_midpoints(mesh, mesh.topology.dim, np.arange(num_cells, dtype=np.int32))\n", + "midpoints = compute_midpoints(\n", + " mesh, mesh.topology.dim, np.arange(num_cells, dtype=np.int32)\n", + ")\n", "\n", "num_dofs = W.dofmap.index_map.size_local + W.dofmap.index_map.num_ghosts\n", - "assert (num_cells == num_dofs)\n", + "assert num_cells == num_dofs\n", "values = np.zeros((num_dofs, 3), dtype=np.float64)\n", - "values[:, :mesh.geometry.dim] = B.x.array.real.reshape(num_dofs, W.dofmap.index_map_bs)\n", + "values[:, : mesh.geometry.dim] = B.x.array.real.reshape(num_dofs, W.dofmap.index_map_bs)\n", "cloud = pyvista.PolyData(midpoints)\n", "cloud[\"B\"] = values\n", "glyphs = cloud.glyph(\"B\", factor=2e6)\n", @@ -457,15 +471,6 @@ "else:\n", " B_fig = plotter.screenshot(\"B.png\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/chapter3/em.py b/chapter3/em.py index de37af0a..deec8059 100644 --- a/chapter3/em.py +++ b/chapter3/em.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -26,7 +26,9 @@ # Through the copper wires a static current of $J=1A$ is flowing. # We would like to compute the magnetic field $B$ in the iron cylinder, the copper wires, and the surrounding vaccum. # -# We start by simplifying the problem to a 2D problem. We can do this by assuming that the cylinder extends far along the z-axis and as a consequence the field is virtually independent of the z-coordinate. +# We start by simplifying the problem to a 2D problem. +# We can do this by assuming that the cylinder extends far along the z-axis and +# as a consequence the field is virtually independent of the z-coordinate. # Next, we consider Maxwell's equation to derive a Poisson equation for the magnetic field (or rather its potential) # # $$ @@ -89,7 +91,13 @@ # + from dolfinx import default_scalar_type -from dolfinx.fem import (dirichletbc, Expression, Function, functionspace, locate_dofs_topological) +from dolfinx.fem import ( + dirichletbc, + Expression, + Function, + functionspace, + locate_dofs_topological, +) from dolfinx.fem.petsc import LinearProblem from dolfinx.io import XDMFFile from dolfinx.io.gmshio import model_to_mesh @@ -106,18 +114,17 @@ rank = MPI.COMM_WORLD.rank gmsh.initialize() -r = 0.1 # Radius of copper wires -R = 5 # Radius of domain -a = 1 # Radius of inner iron cylinder -b = 1.2 # Radius of outer iron cylinder -N = 8 # Number of windings +r = 0.1 # Radius of copper wires +R = 5 # Radius of domain +a = 1 # Radius of inner iron cylinder +b = 1.2 # Radius of outer iron cylinder +N = 8 # Number of windings c_1 = 0.8 # Radius of inner copper wires c_2 = 1.4 # Radius of outer copper wires gdim = 2 # Geometric dimension of the mesh model_rank = 0 mesh_comm = MPI.COMM_WORLD if mesh_comm.rank == model_rank: - # Define geometry for iron cylinder outer_iron = gmsh.model.occ.addCircle(0, 0, 0, b) inner_iron = gmsh.model.occ.addCircle(0, 0, 0, a) @@ -132,11 +139,17 @@ # Define the copper-wires inside iron cylinder angles_N = [i * 2 * np.pi / N for i in range(N)] - wires_N = [(2, gmsh.model.occ.addDisk(c_1 * np.cos(v), c_1 * np.sin(v), 0, r, r)) for v in angles_N] + wires_N = [ + (2, gmsh.model.occ.addDisk(c_1 * np.cos(v), c_1 * np.sin(v), 0, r, r)) + for v in angles_N + ] # Define the copper-wires outside the iron cylinder angles_S = [(i + 0.5) * 2 * np.pi / N for i in range(N)] - wires_S = [(2, gmsh.model.occ.addDisk(c_2 * np.cos(v), c_2 * np.sin(v), 0, r, r)) for v in angles_S] + wires_S = [ + (2, gmsh.model.occ.addDisk(c_2 * np.cos(v), c_2 * np.sin(v), 0, r, r)) + for v in angles_S + ] gmsh.model.occ.synchronize() # Resolve all boundaries of the different wires in the background domain all_surfaces = [(2, iron)] @@ -213,7 +226,7 @@ # We can also visualize the subdommains using pyvista -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) plotter = pyvista.Plotter() tdim = mesh.topology.dim mesh.topology.create_connectivity(tdim, tdim) @@ -276,12 +289,12 @@ # We are now ready to solve the linear problem A_z = Function(V) -problem = LinearProblem(a, L, u=A_z, bcs=[bc]) +problem = LinearProblem(a, L, u=A_z, bcs=[bc], petsc_options_prefix="em_") problem.solve() # As we have computed the magnetic potential, we can now compute the magnetic field, by setting `B=curl(A_z)`. Note that as we have chosen a function space of first order piecewise linear function to describe our potential, the curl of a function in this space is a discontinous zeroth order function (a function of cell-wise constants). We use `dolfinx.fem.Expression` to interpolate the curl into `W`. -W = functionspace(mesh, ("DG", 0, (mesh.geometry.dim, ))) +W = functionspace(mesh, ("DG", 0, (mesh.geometry.dim,))) B = Function(W) B_expr = Expression(as_vector((A_z.dx(1), -A_z.dx(0))), W.element.interpolation_points) B.interpolate(B_expr) @@ -318,12 +331,14 @@ top_imap = mesh.topology.index_map(mesh.topology.dim) num_cells = top_imap.size_local + top_imap.num_ghosts mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim) -midpoints = compute_midpoints(mesh, mesh.topology.dim, np.arange(num_cells, dtype=np.int32)) +midpoints = compute_midpoints( + mesh, mesh.topology.dim, np.arange(num_cells, dtype=np.int32) +) num_dofs = W.dofmap.index_map.size_local + W.dofmap.index_map.num_ghosts -assert (num_cells == num_dofs) +assert num_cells == num_dofs values = np.zeros((num_dofs, 3), dtype=np.float64) -values[:, :mesh.geometry.dim] = B.x.array.real.reshape(num_dofs, W.dofmap.index_map_bs) +values[:, : mesh.geometry.dim] = B.x.array.real.reshape(num_dofs, W.dofmap.index_map_bs) cloud = pyvista.PolyData(midpoints) cloud["B"] = values glyphs = cloud.glyph("B", factor=2e6) @@ -334,6 +349,3 @@ plotter.show() else: B_fig = plotter.screenshot("B.png") -# - - - diff --git a/chapter3/multiple_dirichlet.ipynb b/chapter3/multiple_dirichlet.ipynb index 808cbf65..6e3cec95 100644 --- a/chapter3/multiple_dirichlet.ipynb +++ b/chapter3/multiple_dirichlet.ipynb @@ -36,8 +36,15 @@ "outputs": [], "source": [ "from dolfinx import default_scalar_type\n", - "from dolfinx.fem import (Constant, Function, functionspace,\n", - " assemble_scalar, dirichletbc, form, locate_dofs_geometrical)\n", + "from dolfinx.fem import (\n", + " Constant,\n", + " Function,\n", + " functionspace,\n", + " assemble_scalar,\n", + " dirichletbc,\n", + " form,\n", + " locate_dofs_geometrical,\n", + ")\n", "from dolfinx.fem.petsc import LinearProblem\n", "from dolfinx.mesh import create_unit_square\n", "from dolfinx.plot import vtk_mesh\n", @@ -50,7 +57,7 @@ "\n", "\n", "def u_exact(x):\n", - " return 1 + x[0]**2 + 2 * x[1]**2\n", + " return 1 + x[0] ** 2 + 2 * x[1] ** 2\n", "\n", "\n", "mesh = create_unit_square(MPI.COMM_WORLD, 10, 10)\n", @@ -59,7 +66,7 @@ "v = TestFunction(V)\n", "a = dot(grad(u), grad(v)) * dx\n", "x = SpatialCoordinate(mesh)\n", - "g = - 4 * x[1]\n", + "g = -4 * x[1]\n", "f = Constant(mesh, default_scalar_type(-6))\n", "L = f * v * dx - g * v * ds" ] @@ -79,7 +86,7 @@ "source": [ "dofs_L = locate_dofs_geometrical(V, lambda x: np.isclose(x[0], 0))\n", "u_L = Function(V)\n", - "u_L.interpolate(lambda x: 1 + 2 * x[1]**2)\n", + "u_L.interpolate(lambda x: 1 + 2 * x[1] ** 2)\n", "bc_L = dirichletbc(u_L, dofs_L)" ] }, @@ -98,7 +105,7 @@ "source": [ "dofs_R = locate_dofs_geometrical(V, lambda x: np.isclose(x[0], 1))\n", "u_R = Function(V)\n", - "u_R.interpolate(lambda x: 2 + 2 * x[1]**2)\n", + "u_R.interpolate(lambda x: 2 + 2 * x[1] ** 2)\n", "bc_R = dirichletbc(u_R, dofs_R)\n", "bcs = [bc_R, bc_L]" ] @@ -116,13 +123,19 @@ "metadata": {}, "outputs": [], "source": [ - "problem = LinearProblem(a, L, bcs=bcs, petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"multiple_dirichlet_\",\n", + ")\n", "uh = problem.solve()\n", "\n", "V2 = functionspace(mesh, (\"Lagrange\", 2))\n", "uex = Function(V2)\n", "uex.interpolate(u_exact)\n", - "error_L2 = assemble_scalar(form((uh - uex)**2 * dx))\n", + "error_L2 = assemble_scalar(form((uh - uex) ** 2 * dx))\n", "error_L2 = np.sqrt(MPI.COMM_WORLD.allreduce(error_L2, op=MPI.SUM))\n", "\n", "u_vertex_values = uh.x.array\n", @@ -149,7 +162,7 @@ "metadata": {}, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "pyvista_cells, cell_types, geometry = vtk_mesh(V)\n", "grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry)\n", "grid.point_data[\"u\"] = uh.x.array\n", diff --git a/chapter3/multiple_dirichlet.py b/chapter3/multiple_dirichlet.py index 628b41f3..7c86565d 100644 --- a/chapter3/multiple_dirichlet.py +++ b/chapter3/multiple_dirichlet.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -39,8 +39,15 @@ # + from dolfinx import default_scalar_type -from dolfinx.fem import (Constant, Function, functionspace, - assemble_scalar, dirichletbc, form, locate_dofs_geometrical) +from dolfinx.fem import ( + Constant, + Function, + functionspace, + assemble_scalar, + dirichletbc, + form, + locate_dofs_geometrical, +) from dolfinx.fem.petsc import LinearProblem from dolfinx.mesh import create_unit_square from dolfinx.plot import vtk_mesh @@ -53,7 +60,7 @@ def u_exact(x): - return 1 + x[0]**2 + 2 * x[1]**2 + return 1 + x[0] ** 2 + 2 * x[1] ** 2 mesh = create_unit_square(MPI.COMM_WORLD, 10, 10) @@ -62,7 +69,7 @@ def u_exact(x): v = TestFunction(V) a = dot(grad(u), grad(v)) * dx x = SpatialCoordinate(mesh) -g = - 4 * x[1] +g = -4 * x[1] f = Constant(mesh, default_scalar_type(-6)) L = f * v * dx - g * v * ds # - @@ -71,27 +78,33 @@ def u_exact(x): dofs_L = locate_dofs_geometrical(V, lambda x: np.isclose(x[0], 0)) u_L = Function(V) -u_L.interpolate(lambda x: 1 + 2 * x[1]**2) +u_L.interpolate(lambda x: 1 + 2 * x[1] ** 2) bc_L = dirichletbc(u_L, dofs_L) # Note that we have used `lambda`-functions to compactly define the functions returning the subdomain evaluation and function evaluation. We can use a similar procedure for the right boundary condition, and gather both boundary conditions in a vector `bcs`. dofs_R = locate_dofs_geometrical(V, lambda x: np.isclose(x[0], 1)) u_R = Function(V) -u_R.interpolate(lambda x: 2 + 2 * x[1]**2) +u_R.interpolate(lambda x: 2 + 2 * x[1] ** 2) bc_R = dirichletbc(u_R, dofs_R) bcs = [bc_R, bc_L] # We are now ready to again solve the problem, and check the $L^2$ and max error at the mesh vertices. # + -problem = LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) +problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="multiple_dirichlet_", +) uh = problem.solve() V2 = functionspace(mesh, ("Lagrange", 2)) uex = Function(V2) uex.interpolate(u_exact) -error_L2 = assemble_scalar(form((uh - uex)**2 * dx)) +error_L2 = assemble_scalar(form((uh - uex) ** 2 * dx)) error_L2 = np.sqrt(MPI.COMM_WORLD.allreduce(error_L2, op=MPI.SUM)) u_vertex_values = uh.x.array @@ -108,7 +121,7 @@ def u_exact(x): # To visualize the solution, run the script with in a Jupyter notebook with `off_screen=False` or as a python script with `off_screen=True`. # + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) pyvista_cells, cell_types, geometry = vtk_mesh(V) grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry) grid.point_data["u"] = uh.x.array diff --git a/chapter3/neumann_dirichlet_code.ipynb b/chapter3/neumann_dirichlet_code.ipynb index ff994a36..7718f599 100644 --- a/chapter3/neumann_dirichlet_code.ipynb +++ b/chapter3/neumann_dirichlet_code.ipynb @@ -9,8 +9,11 @@ "# Combining Dirichlet and Neumann conditions\n", "Author: Jørgen S. Dokken\n", "\n", - "Let's return to the Poisson problem from the [Fundamentals chapter](./../chapter1/fundamentals.md) and see how to extend the mathematics and the implementation to handle Dirichlet condition in combination with a Neumann condition.\n", - "The domain is still the unit square, but now we set the Dirichlet condition $u=u_D$ at the left and right sides, while the Neumann condition\n", + "Let's return to the Poisson problem from the [Fundamentals chapter](./../chapter1/fundamentals.md)\n", + "and see how to extend the mathematics and the implementation to handle Dirichlet condition\n", + "in combination with a Neumann condition.\n", + "The domain is still the unit square, but now we set the Dirichlet condition $u=u_D$ at the left and right sides,\n", + "while the Neumann condition\n", "\n", "$$\n", "-\\frac{\\partial u}{\\partial n}=g\n", @@ -19,7 +22,8 @@ "is applied to the remaining sides $y=0$ and $y=1$.\n", "\n", "## The PDE problem\n", - "Let $\\Lambda_D$ and $\\Lambda_N$ denote parts of the boundary $\\partial \\Omega$ where the Dirichlet and Neumann conditions apply, respectively.\n", + "Let $\\Lambda_D$ and $\\Lambda_N$ denote parts of the boundary $\\partial \\Omega$\n", + "where the Dirichlet and Neumann conditions apply, respectively.\n", "The complete boundary-value problem can be written as\n", "\n", "$$\n", @@ -47,30 +51,36 @@ "u_D(x,y)=1+x^2+2y^2.\n", "$$\n", "\n", - "For the ease of programming, we define $g$ as a function over the whole domain $\\Omega$ such that $g$ takes on the correct values at $y=0$ and $y=1$. One possible extension is\n", + "For the ease of programming, we define $g$ as a function over the whole domain $\\Omega$ such that\n", + "$g$ takes on the correct values at $y=0$ and $y=1$. One possible extension is\n", "\n", "$$\n", " g(x,y)=-4y.\n", "$$\n", "\n", "## The variational formulation\n", - "The first task is to derive the variational formulatin. This time we cannot omit the boundary term arising from integration by parts, because $v$ is only zero on $\\Lambda_D$. We have\n", + "The first task is to derive the variational formulation.\n", + "This time we cannot omit the boundary term arising from integration by parts,\n", + "because $v$ is only zero on $\\Lambda_D$. We have\n", "\n", "$$\n", - "-\\int_\\Omega (\\nabla^2u)v~\\mathrm{d} x = \\int_\\Omega \\nabla u \\cdot \\nabla v ~\\mathrm{d} x - \\int_{\\partial\\Omega}\\frac{\\partial u}{\\partial n}v~\\mathrm{d}s,\n", + "-\\int_\\Omega (\\nabla^2u)v~\\mathrm{d} x =\n", + "\\int_\\Omega \\nabla u \\cdot \\nabla v ~\\mathrm{d} x - \\int_{\\partial\\Omega}\\frac{\\partial u}{\\partial n}v~\\mathrm{d}s,\n", "$$\n", "\n", "and since $v=0$ on $\\Lambda_D$,\n", "\n", "$$\n", - "- \\int_{\\partial\\Omega}\\frac{\\partial u}{\\partial n}v~\\mathrm{d}s= - \\int_{\\Lambda_N}\\frac{\\partial u}{\\partial n}v~\\mathrm{d}s =\\int_{\\Lambda_N} gv~\\mathrm{d}s,\n", + "- \\int_{\\partial\\Omega}\\frac{\\partial u}{\\partial n}v~\\mathrm{d}s=\n", + "- \\int_{\\Lambda_N}\\frac{\\partial u}{\\partial n}v~\\mathrm{d}s =\\int_{\\Lambda_N} gv~\\mathrm{d}s,\n", "$$\n", "\n", "by applying the boundary condition on $\\Lambda_N$.\n", "The resulting weak from reads\n", "\n", "$$\n", - " \\int_\\Omega \\nabla u \\cdot \\nabla v~\\mathrm{d} x = \\int_\\Omega fv~\\mathrm{d} x - \\int_{\\Lambda_N}gv~\\mathrm{d}s.\n", + "\\int_\\Omega \\nabla u \\cdot \\nabla v~\\mathrm{d} x =\n", + "\\int_\\Omega fv~\\mathrm{d} x - \\int_{\\Lambda_N}gv~\\mathrm{d}s.\n", "$$\n", "Expressing this equation in the standard notation $a(u,v)=L(v)$ is straight-forward with\n", "\n", @@ -92,8 +102,15 @@ "outputs": [], "source": [ "from dolfinx import default_scalar_type\n", - "from dolfinx.fem import (Constant, Function, functionspace,\n", - " assemble_scalar, dirichletbc, form, locate_dofs_geometrical)\n", + "from dolfinx.fem import (\n", + " Constant,\n", + " Function,\n", + " functionspace,\n", + " assemble_scalar,\n", + " dirichletbc,\n", + " form,\n", + " locate_dofs_geometrical,\n", + ")\n", "from dolfinx.fem.petsc import LinearProblem\n", "from dolfinx.mesh import create_unit_square\n", "from dolfinx.plot import vtk_mesh\n", @@ -113,7 +130,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "Now we get to the Neumann and Dirichlet boundary condition. As previously, we use a Python-function to define the boundary where we should have a Dirichlet condition. Then, with this function, we locate degrees of freedom that fulfill this condition." ] @@ -125,7 +144,7 @@ "outputs": [], "source": [ "def u_exact(x):\n", - " return 1 + x[0]**2 + 2 * x[1]**2\n", + " return 1 + x[0] ** 2 + 2 * x[1] ** 2\n", "\n", "\n", "def boundary_D(x):\n", @@ -170,13 +189,19 @@ "metadata": {}, "outputs": [], "source": [ - "problem = LinearProblem(a, L, bcs=[bc], petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=[bc],\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"neumann_dirichlet_\",\n", + ")\n", "uh = problem.solve()\n", "\n", "V2 = functionspace(mesh, (\"Lagrange\", 2))\n", "uex = Function(V2)\n", "uex.interpolate(u_exact)\n", - "error_L2 = assemble_scalar(form((uh - uex)**2 * dx))\n", + "error_L2 = assemble_scalar(form((uh - uex) ** 2 * dx))\n", "error_L2 = np.sqrt(MPI.COMM_WORLD.allreduce(error_L2, op=MPI.SUM))\n", "\n", "u_vertex_values = uh.x.array\n", @@ -197,6 +222,16 @@ "To look at the actual solution, run the script as a python script with `off_screen=True` or as a Jupyter notebook with `off_screen=False`" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa236113", + "metadata": {}, + "outputs": [], + "source": [ + "pyvista.start_xvfb(1.0)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -205,8 +240,6 @@ }, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", - "\n", "pyvista_cells, cell_types, geometry = vtk_mesh(V)\n", "grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry)\n", "grid.point_data[\"u\"] = uh.x.array\n", diff --git a/chapter3/neumann_dirichlet_code.py b/chapter3/neumann_dirichlet_code.py index e5ed35e1..dcd11cd0 100644 --- a/chapter3/neumann_dirichlet_code.py +++ b/chapter3/neumann_dirichlet_code.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -16,8 +16,11 @@ # # Combining Dirichlet and Neumann conditions # Author: Jørgen S. Dokken # -# Let's return to the Poisson problem from the [Fundamentals chapter](./../chapter1/fundamentals.md) and see how to extend the mathematics and the implementation to handle Dirichlet condition in combination with a Neumann condition. -# The domain is still the unit square, but now we set the Dirichlet condition $u=u_D$ at the left and right sides, while the Neumann condition +# Let's return to the Poisson problem from the [Fundamentals chapter](./../chapter1/fundamentals.md) +# and see how to extend the mathematics and the implementation to handle Dirichlet condition +# in combination with a Neumann condition. +# The domain is still the unit square, but now we set the Dirichlet condition $u=u_D$ at the left and right sides, +# while the Neumann condition # # $$ # -\frac{\partial u}{\partial n}=g @@ -26,7 +29,8 @@ # is applied to the remaining sides $y=0$ and $y=1$. # # ## The PDE problem -# Let $\Lambda_D$ and $\Lambda_N$ denote parts of the boundary $\partial \Omega$ where the Dirichlet and Neumann conditions apply, respectively. +# Let $\Lambda_D$ and $\Lambda_N$ denote parts of the boundary $\partial \Omega$ +# where the Dirichlet and Neumann conditions apply, respectively. # The complete boundary-value problem can be written as # # $$ @@ -54,30 +58,36 @@ # u_D(x,y)=1+x^2+2y^2. # $$ # -# For the ease of programming, we define $g$ as a function over the whole domain $\Omega$ such that $g$ takes on the correct values at $y=0$ and $y=1$. One possible extension is +# For the ease of programming, we define $g$ as a function over the whole domain $\Omega$ such that +# $g$ takes on the correct values at $y=0$ and $y=1$. One possible extension is # # $$ # g(x,y)=-4y. # $$ # # ## The variational formulation -# The first task is to derive the variational formulatin. This time we cannot omit the boundary term arising from integration by parts, because $v$ is only zero on $\Lambda_D$. We have +# The first task is to derive the variational formulation. +# This time we cannot omit the boundary term arising from integration by parts, +# because $v$ is only zero on $\Lambda_D$. We have # # $$ -# -\int_\Omega (\nabla^2u)v~\mathrm{d} x = \int_\Omega \nabla u \cdot \nabla v ~\mathrm{d} x - \int_{\partial\Omega}\frac{\partial u}{\partial n}v~\mathrm{d}s, +# -\int_\Omega (\nabla^2u)v~\mathrm{d} x = +# \int_\Omega \nabla u \cdot \nabla v ~\mathrm{d} x - \int_{\partial\Omega}\frac{\partial u}{\partial n}v~\mathrm{d}s, # $$ # # and since $v=0$ on $\Lambda_D$, # # $$ -# - \int_{\partial\Omega}\frac{\partial u}{\partial n}v~\mathrm{d}s= - \int_{\Lambda_N}\frac{\partial u}{\partial n}v~\mathrm{d}s =\int_{\Lambda_N} gv~\mathrm{d}s, +# - \int_{\partial\Omega}\frac{\partial u}{\partial n}v~\mathrm{d}s= +# - \int_{\Lambda_N}\frac{\partial u}{\partial n}v~\mathrm{d}s =\int_{\Lambda_N} gv~\mathrm{d}s, # $$ # # by applying the boundary condition on $\Lambda_N$. # The resulting weak from reads # # $$ -# \int_\Omega \nabla u \cdot \nabla v~\mathrm{d} x = \int_\Omega fv~\mathrm{d} x - \int_{\Lambda_N}gv~\mathrm{d}s. +# \int_\Omega \nabla u \cdot \nabla v~\mathrm{d} x = +# \int_\Omega fv~\mathrm{d} x - \int_{\Lambda_N}gv~\mathrm{d}s. # $$ # Expressing this equation in the standard notation $a(u,v)=L(v)$ is straight-forward with # @@ -93,8 +103,15 @@ # + from dolfinx import default_scalar_type -from dolfinx.fem import (Constant, Function, functionspace, - assemble_scalar, dirichletbc, form, locate_dofs_geometrical) +from dolfinx.fem import ( + Constant, + Function, + functionspace, + assemble_scalar, + dirichletbc, + form, + locate_dofs_geometrical, +) from dolfinx.fem.petsc import LinearProblem from dolfinx.mesh import create_unit_square from dolfinx.plot import vtk_mesh @@ -110,15 +127,14 @@ u = TrialFunction(V) v = TestFunction(V) a = dot(grad(u), grad(v)) * dx - - # - # Now we get to the Neumann and Dirichlet boundary condition. As previously, we use a Python-function to define the boundary where we should have a Dirichlet condition. Then, with this function, we locate degrees of freedom that fulfill this condition. + # + def u_exact(x): - return 1 + x[0]**2 + 2 * x[1]**2 + return 1 + x[0] ** 2 + 2 * x[1] ** 2 def boundary_D(x): @@ -141,13 +157,19 @@ def boundary_D(x): # We can now assemble and solve the linear system of equations # + -problem = LinearProblem(a, L, bcs=[bc], petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) +problem = LinearProblem( + a, + L, + bcs=[bc], + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="neumann_dirichlet_", +) uh = problem.solve() V2 = functionspace(mesh, ("Lagrange", 2)) uex = Function(V2) uex.interpolate(u_exact) -error_L2 = assemble_scalar(form((uh - uex)**2 * dx)) +error_L2 = assemble_scalar(form((uh - uex) ** 2 * dx)) error_L2 = np.sqrt(MPI.COMM_WORLD.allreduce(error_L2, op=MPI.SUM)) u_vertex_values = uh.x.array @@ -163,9 +185,9 @@ def boundary_D(x): # ## Visualization # To look at the actual solution, run the script as a python script with `off_screen=True` or as a Jupyter notebook with `off_screen=False` -# + -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) +# + pyvista_cells, cell_types, geometry = vtk_mesh(V) grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry) grid.point_data["u"] = uh.x.array diff --git a/chapter3/robin_neumann_dirichlet.ipynb b/chapter3/robin_neumann_dirichlet.ipynb index ce17f02c..5b281792 100644 --- a/chapter3/robin_neumann_dirichlet.ipynb +++ b/chapter3/robin_neumann_dirichlet.ipynb @@ -16,7 +16,7 @@ "- $\\Gamma_R$ for Robin conditions: $-\\kappa \\frac{\\partial u}{\\partial n}=r(u-s)$\n", "\n", "where $r$ and $s$ are specified functions. The Robin condition is most often used to model heat transfer to the surroundings and arises naturally from Newton's cooling law.\n", - "In that case, $r$ is a heat transfer coefficient, and $s$ is the temperature of the surroundings. \n", + "In that case, $r$ is a heat transfer coefficient, and $s$ is the temperature of the surroundings.\n", "Both can be space and time-dependent. The Robin conditions apply at some parts $\\Gamma_R^0,\\Gamma_R^1,\\dots$, of the boundary:\n", "\n", "$$\n", @@ -37,7 +37,7 @@ "-\\kappa \\frac{\\partial u}{\\partial n}=g_j \\quad\\text{on } \\Gamma_N^j,\n", "$$\n", "$$\n", - "-\\kappa \\frac{\\partial u}{\\partial n}=r_k(u-s_k)\\quad \\text{ on } \\Gamma_R^k, \n", + "-\\kappa \\frac{\\partial u}{\\partial n}=r_k(u-s_k)\\quad \\text{ on } \\Gamma_R^k,\n", "$$\n", "\n", "As usual, we multiply by a test function and integrate by parts.\n", @@ -58,7 +58,7 @@ "F(u, v)=\\int_\\Omega \\kappa \\nabla u \\cdot \\nabla v~\\mathrm{d} x + \\sum_i\\int_{\\Gamma_N^i}g_i v~\\mathrm{d}s +\\sum_i\\int_{\\Gamma_R^i}r_i(u-s_i)~\\mathrm{d}s - \\int_\\Omega fv~\\mathrm{d} x = 0.\n", "$$\n", "\n", - "We have been used to writing the variational formulation as $a(u,v)=L(v)$, which requires that we identify the integrals dependent on the trial function $u$ and collect these in $a(u,v)$, while the remaining terms form $L(v)$. We note that the Robin condition has a contribution to both $a(u,v)$ and $L(v)$. \n", + "We have been used to writing the variational formulation as $a(u,v)=L(v)$, which requires that we identify the integrals dependent on the trial function $u$ and collect these in $a(u,v)$, while the remaining terms form $L(v)$. We note that the Robin condition has a contribution to both $a(u,v)$ and $L(v)$.\n", "We then have\n", "\n", "$$\n", @@ -86,16 +86,35 @@ "outputs": [], "source": [ "from dolfinx import default_scalar_type\n", - "from dolfinx.fem import (Constant, Function, functionspace, assemble_scalar, \n", - " dirichletbc, form, locate_dofs_topological)\n", + "from dolfinx.fem import (\n", + " Constant,\n", + " Function,\n", + " functionspace,\n", + " assemble_scalar,\n", + " dirichletbc,\n", + " form,\n", + " locate_dofs_topological,\n", + ")\n", "from dolfinx.fem.petsc import LinearProblem\n", "from dolfinx.io import XDMFFile\n", "from dolfinx.mesh import create_unit_square, locate_entities, meshtags\n", "from dolfinx.plot import vtk_mesh\n", "\n", "from mpi4py import MPI\n", - "from ufl import (FacetNormal, Measure, SpatialCoordinate, TestFunction, TrialFunction, \n", - " div, dot, dx, grad, inner, lhs, rhs)\n", + "from ufl import (\n", + " FacetNormal,\n", + " Measure,\n", + " SpatialCoordinate,\n", + " TestFunction,\n", + " TrialFunction,\n", + " div,\n", + " dot,\n", + " dx,\n", + " grad,\n", + " inner,\n", + " lhs,\n", + " rhs,\n", + ")\n", "\n", "import numpy as np\n", "import pyvista\n", @@ -105,7 +124,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "In this section, we will solve the Poisson problem for the manufactured solution $u_{ex} = 1+x^2+2y^2$, which yields $\\kappa=1$, $f=-6$. The next step is to define the parameters of the boundary condition, and where we should apply them. In this example, we will apply the following\n", "\n", @@ -119,7 +140,7 @@ "-\\kappa \\frac{\\partial u}{\\partial n} =g_0 \\quad\\text{for } y = 1\n", "$$\n", "\n", - "To reproduce the analytical solution, we have that \n", + "To reproduce the analytical solution, we have that\n", "\n", "$$\n", " u_D=u_{ex}=1+x^2+2y^2\n", @@ -140,7 +161,10 @@ "metadata": {}, "outputs": [], "source": [ - "u_ex = lambda x: 1 + x[0]**2 + 2*x[1]**2\n", + "def u_ex(x):\n", + " return 1 + x[0] ** 2 + 2 * x[1] ** 2\n", + "\n", + "\n", "x = SpatialCoordinate(mesh)\n", "# Define physical parameters and boundary condtions\n", "s = u_ex(x)\n", @@ -168,10 +192,12 @@ "metadata": {}, "outputs": [], "source": [ - "boundaries = [(1, lambda x: np.isclose(x[0], 0)),\n", - " (2, lambda x: np.isclose(x[0], 1)),\n", - " (3, lambda x: np.isclose(x[1], 0)),\n", - " (4, lambda x: np.isclose(x[1], 1))]" + "boundaries = [\n", + " (1, lambda x: np.isclose(x[0], 0)),\n", + " (2, lambda x: np.isclose(x[0], 1)),\n", + " (3, lambda x: np.isclose(x[1], 0)),\n", + " (4, lambda x: np.isclose(x[1], 1)),\n", + "]" ] }, { @@ -189,14 +215,16 @@ "source": [ "facet_indices, facet_markers = [], []\n", "fdim = mesh.topology.dim - 1\n", - "for (marker, locator) in boundaries:\n", + "for marker, locator in boundaries:\n", " facets = locate_entities(mesh, fdim, locator)\n", " facet_indices.append(facets)\n", " facet_markers.append(np.full_like(facets, marker))\n", "facet_indices = np.hstack(facet_indices).astype(np.int32)\n", "facet_markers = np.hstack(facet_markers).astype(np.int32)\n", "sorted_facets = np.argsort(facet_indices)\n", - "facet_tag = meshtags(mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets])" + "facet_tag = meshtags(\n", + " mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets]\n", + ")" ] }, { @@ -213,7 +241,7 @@ "metadata": {}, "outputs": [], "source": [ - "mesh.topology.create_connectivity(mesh.topology.dim-1, mesh.topology.dim)\n", + "mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim)\n", "with XDMFFile(mesh.comm, \"facet_tags.xdmf\", \"w\") as xdmf:\n", " xdmf.write_mesh(mesh)\n", " xdmf.write_meshtags(facet_tag, mesh.geometry)" @@ -229,7 +257,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "ds = Measure(\"ds\", domain=mesh, subdomain_data=facet_tag)" @@ -237,7 +267,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "We can now create a general boundary condition class." ] @@ -248,7 +280,7 @@ "metadata": {}, "outputs": [], "source": [ - "class BoundaryCondition():\n", + "class BoundaryCondition:\n", " def __init__(self, type, marker, values):\n", " self._type = type\n", " if type == \"Dirichlet\":\n", @@ -258,11 +290,12 @@ " dofs = locate_dofs_topological(V, fdim, facets)\n", " self._bc = dirichletbc(u_D, dofs)\n", " elif type == \"Neumann\":\n", - " self._bc = inner(values, v) * ds(marker)\n", + " self._bc = inner(values, v) * ds(marker)\n", " elif type == \"Robin\":\n", - " self._bc = values[0] * inner(u-values[1], v)* ds(marker)\n", + " self._bc = values[0] * inner(u - values[1], v) * ds(marker)\n", " else:\n", " raise TypeError(\"Unknown boundary condition: {0:s}\".format(type))\n", + "\n", " @property\n", " def bc(self):\n", " return self._bc\n", @@ -271,11 +304,14 @@ " def type(self):\n", " return self._type\n", "\n", + "\n", "# Define the Dirichlet condition\n", - "boundary_conditions = [BoundaryCondition(\"Dirichlet\", 1, u_ex),\n", - " BoundaryCondition(\"Dirichlet\", 2, u_ex),\n", - " BoundaryCondition(\"Robin\", 3, (r, s)),\n", - " BoundaryCondition(\"Neumann\", 4, g)]\n" + "boundary_conditions = [\n", + " BoundaryCondition(\"Dirichlet\", 1, u_ex),\n", + " BoundaryCondition(\"Dirichlet\", 2, u_ex),\n", + " BoundaryCondition(\"Robin\", 3, (r, s)),\n", + " BoundaryCondition(\"Neumann\", 4, g),\n", + "]\n" ] }, { @@ -315,11 +351,17 @@ "# Solve linear variational problem\n", "a = lhs(F)\n", "L = rhs(F)\n", - "problem = LinearProblem(a, L, bcs=bcs, petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"robin_neumann_dirichlet_\",\n", + ")\n", "uh = problem.solve()\n", "\n", "# Visualize solution\n", - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "pyvista_cells, cell_types, geometry = vtk_mesh(V)\n", "grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry)\n", "grid.point_data[\"u\"] = uh.x.array\n", @@ -346,16 +388,16 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "lines_to_next_cell": 0 - }, + "metadata": {}, "outputs": [], "source": [ "# Compute L2 error and error at nodes\n", "V_ex = functionspace(mesh, (\"Lagrange\", 2))\n", "u_exact = Function(V_ex)\n", "u_exact.interpolate(u_ex)\n", - "error_L2 = np.sqrt(mesh.comm.allreduce(assemble_scalar(form((uh - u_exact)**2 * dx)), op=MPI.SUM))\n", + "error_L2 = np.sqrt(\n", + " mesh.comm.allreduce(assemble_scalar(form((uh - u_exact) ** 2 * dx)), op=MPI.SUM)\n", + ")\n", "\n", "u_vertex_values = uh.x.array\n", "uex_1 = Function(V)\n", @@ -366,15 +408,6 @@ "print(f\"Error_L2 : {error_L2:.2e}\")\n", "print(f\"Error_max : {error_max:.2e}\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/chapter3/robin_neumann_dirichlet.py b/chapter3/robin_neumann_dirichlet.py index c2530a4c..6903ae1f 100644 --- a/chapter3/robin_neumann_dirichlet.py +++ b/chapter3/robin_neumann_dirichlet.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -25,7 +25,7 @@ # - $\Gamma_R$ for Robin conditions: $-\kappa \frac{\partial u}{\partial n}=r(u-s)$ # # where $r$ and $s$ are specified functions. The Robin condition is most often used to model heat transfer to the surroundings and arises naturally from Newton's cooling law. -# In that case, $r$ is a heat transfer coefficient, and $s$ is the temperature of the surroundings. +# In that case, $r$ is a heat transfer coefficient, and $s$ is the temperature of the surroundings. # Both can be space and time-dependent. The Robin conditions apply at some parts $\Gamma_R^0,\Gamma_R^1,\dots$, of the boundary: # # $$ @@ -46,7 +46,7 @@ # -\kappa \frac{\partial u}{\partial n}=g_j \quad\text{on } \Gamma_N^j, # $$ # $$ -# -\kappa \frac{\partial u}{\partial n}=r_k(u-s_k)\quad \text{ on } \Gamma_R^k, +# -\kappa \frac{\partial u}{\partial n}=r_k(u-s_k)\quad \text{ on } \Gamma_R^k, # $$ # # As usual, we multiply by a test function and integrate by parts. @@ -67,7 +67,7 @@ # F(u, v)=\int_\Omega \kappa \nabla u \cdot \nabla v~\mathrm{d} x + \sum_i\int_{\Gamma_N^i}g_i v~\mathrm{d}s +\sum_i\int_{\Gamma_R^i}r_i(u-s_i)~\mathrm{d}s - \int_\Omega fv~\mathrm{d} x = 0. # $$ # -# We have been used to writing the variational formulation as $a(u,v)=L(v)$, which requires that we identify the integrals dependent on the trial function $u$ and collect these in $a(u,v)$, while the remaining terms form $L(v)$. We note that the Robin condition has a contribution to both $a(u,v)$ and $L(v)$. +# We have been used to writing the variational formulation as $a(u,v)=L(v)$, which requires that we identify the integrals dependent on the trial function $u$ and collect these in $a(u,v)$, while the remaining terms form $L(v)$. We note that the Robin condition has a contribution to both $a(u,v)$ and $L(v)$. # We then have # # $$ @@ -84,16 +84,35 @@ # + from dolfinx import default_scalar_type -from dolfinx.fem import (Constant, Function, functionspace, assemble_scalar, - dirichletbc, form, locate_dofs_topological) +from dolfinx.fem import ( + Constant, + Function, + functionspace, + assemble_scalar, + dirichletbc, + form, + locate_dofs_topological, +) from dolfinx.fem.petsc import LinearProblem from dolfinx.io import XDMFFile from dolfinx.mesh import create_unit_square, locate_entities, meshtags from dolfinx.plot import vtk_mesh from mpi4py import MPI -from ufl import (FacetNormal, Measure, SpatialCoordinate, TestFunction, TrialFunction, - div, dot, dx, grad, inner, lhs, rhs) +from ufl import ( + FacetNormal, + Measure, + SpatialCoordinate, + TestFunction, + TrialFunction, + div, + dot, + dx, + grad, + inner, + lhs, + rhs, +) import numpy as np import pyvista @@ -113,7 +132,7 @@ # -\kappa \frac{\partial u}{\partial n} =g_0 \quad\text{for } y = 1 # $$ # -# To reproduce the analytical solution, we have that +# To reproduce the analytical solution, we have that # # $$ # u_D=u_{ex}=1+x^2+2y^2 @@ -127,7 +146,12 @@ # we can specify $r\neq 0$ arbitrarily and $s=u_{ex}$. We choose $r=1000$. # We can now create all the necessary variable definitions and the traditional part of the variational form. -u_ex = lambda x: 1 + x[0]**2 + 2*x[1]**2 + +# + +def u_ex(x): + return 1 + x[0] ** 2 + 2 * x[1] ** 2 + + x = SpatialCoordinate(mesh) # Define physical parameters and boundary condtions s = u_ex(x) @@ -140,31 +164,36 @@ V = functionspace(mesh, ("Lagrange", 1)) u, v = TrialFunction(V), TestFunction(V) F = kappa * inner(grad(u), grad(v)) * dx - inner(f, v) * dx +# - # We start by identifying the facets contained in each boundary and create a custom integration measure `ds`. -boundaries = [(1, lambda x: np.isclose(x[0], 0)), - (2, lambda x: np.isclose(x[0], 1)), - (3, lambda x: np.isclose(x[1], 0)), - (4, lambda x: np.isclose(x[1], 1))] +boundaries = [ + (1, lambda x: np.isclose(x[0], 0)), + (2, lambda x: np.isclose(x[0], 1)), + (3, lambda x: np.isclose(x[1], 0)), + (4, lambda x: np.isclose(x[1], 1)), +] # We now loop through all the boundary conditions and create `MeshTags` identifying the facets for each boundary condition. facet_indices, facet_markers = [], [] fdim = mesh.topology.dim - 1 -for (marker, locator) in boundaries: +for marker, locator in boundaries: facets = locate_entities(mesh, fdim, locator) facet_indices.append(facets) facet_markers.append(np.full_like(facets, marker)) facet_indices = np.hstack(facet_indices).astype(np.int32) facet_markers = np.hstack(facet_markers).astype(np.int32) sorted_facets = np.argsort(facet_indices) -facet_tag = meshtags(mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets]) +facet_tag = meshtags( + mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets] +) # ## Debugging boundary condition # To debug boundary conditions, the easiest thing to do is to visualize the boundary in Paraview by writing the `MeshTags` to file. We can then inspect individual boundaries using the `Threshold`-filter. -mesh.topology.create_connectivity(mesh.topology.dim-1, mesh.topology.dim) +mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim) with XDMFFile(mesh.comm, "facet_tags.xdmf", "w") as xdmf: xdmf.write_mesh(mesh) xdmf.write_meshtags(facet_tag, mesh.geometry) @@ -176,8 +205,9 @@ # We can now create a general boundary condition class. + # + -class BoundaryCondition(): +class BoundaryCondition: def __init__(self, type, marker, values): self._type = type if type == "Dirichlet": @@ -187,11 +217,12 @@ def __init__(self, type, marker, values): dofs = locate_dofs_topological(V, fdim, facets) self._bc = dirichletbc(u_D, dofs) elif type == "Neumann": - self._bc = inner(values, v) * ds(marker) + self._bc = inner(values, v) * ds(marker) elif type == "Robin": - self._bc = values[0] * inner(u-values[1], v)* ds(marker) + self._bc = values[0] * inner(u - values[1], v) * ds(marker) else: raise TypeError("Unknown boundary condition: {0:s}".format(type)) + @property def bc(self): return self._bc @@ -200,11 +231,14 @@ def bc(self): def type(self): return self._type + # Define the Dirichlet condition -boundary_conditions = [BoundaryCondition("Dirichlet", 1, u_ex), - BoundaryCondition("Dirichlet", 2, u_ex), - BoundaryCondition("Robin", 3, (r, s)), - BoundaryCondition("Neumann", 4, g)] +boundary_conditions = [ + BoundaryCondition("Dirichlet", 1, u_ex), + BoundaryCondition("Dirichlet", 2, u_ex), + BoundaryCondition("Robin", 3, (r, s)), + BoundaryCondition("Neumann", 4, g), +] # - @@ -223,11 +257,17 @@ def type(self): # Solve linear variational problem a = lhs(F) L = rhs(F) -problem = LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) +problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="robin_neumann_dirichlet_", +) uh = problem.solve() # Visualize solution -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) pyvista_cells, cell_types, geometry = vtk_mesh(V) grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, geometry) grid.point_data["u"] = uh.x.array @@ -251,7 +291,9 @@ def type(self): V_ex = functionspace(mesh, ("Lagrange", 2)) u_exact = Function(V_ex) u_exact.interpolate(u_ex) -error_L2 = np.sqrt(mesh.comm.allreduce(assemble_scalar(form((uh - u_exact)**2 * dx)), op=MPI.SUM)) +error_L2 = np.sqrt( + mesh.comm.allreduce(assemble_scalar(form((uh - u_exact) ** 2 * dx)), op=MPI.SUM) +) u_vertex_values = uh.x.array uex_1 = Function(V) @@ -261,6 +303,3 @@ def type(self): error_max = mesh.comm.allreduce(error_max, op=MPI.MAX) print(f"Error_L2 : {error_L2:.2e}") print(f"Error_max : {error_max:.2e}") -# - - - diff --git a/chapter3/subdomains.ipynb b/chapter3/subdomains.ipynb index 7aaed31a..84c77e19 100644 --- a/chapter3/subdomains.ipynb +++ b/chapter3/subdomains.ipynb @@ -45,7 +45,7 @@ "import numpy as np\n", "import pyvista\n", "\n", - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "\n", "mesh = create_unit_square(MPI.COMM_WORLD, 10, 10)\n", "Q = functionspace(mesh, (\"DG\", 0))" @@ -165,7 +165,11 @@ "outputs": [], "source": [ "problem = LinearProblem(\n", - " a, L, bcs=bcs, petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"}\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"subdomains_structured_\",\n", ")\n", "uh = problem.solve()\n", "\n", @@ -490,7 +494,11 @@ "L = Constant(mesh, default_scalar_type(1)) * v * dx\n", "\n", "problem = LinearProblem(\n", - " a, L, bcs=bcs, petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"}\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"subdomains_unstructured_\",\n", ")\n", "uh = problem.solve()\n", "\n", diff --git a/chapter3/subdomains.py b/chapter3/subdomains.py index 2bf8b046..9405b9f7 100644 --- a/chapter3/subdomains.py +++ b/chapter3/subdomains.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -46,7 +46,7 @@ import numpy as np import pyvista -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) mesh = create_unit_square(MPI.COMM_WORLD, 10, 10) Q = functionspace(mesh, ("DG", 0)) @@ -111,7 +111,11 @@ def Omega_1(x): # + problem = LinearProblem( - a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"} + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="subdomains_structured_", ) uh = problem.solve() @@ -296,7 +300,11 @@ def create_mesh(mesh, cell_type, prune_z=False): L = Constant(mesh, default_scalar_type(1)) * v * dx problem = LinearProblem( - a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"} + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="subdomains_unstructured_", ) uh = problem.solve() diff --git a/chapter4/compiler_parameters.ipynb b/chapter4/compiler_parameters.ipynb index c067f1f7..899e2366 100644 --- a/chapter4/compiler_parameters.ipynb +++ b/chapter4/compiler_parameters.ipynb @@ -26,11 +26,9 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "import seaborn\n", "import time\n", - "import ufl\n", "\n", "from ufl import TestFunction, TrialFunction, dx, inner\n", "from dolfinx.mesh import create_unit_cube\n", @@ -50,7 +48,8 @@ "id": "2", "metadata": {}, "source": [ - "Next we generate a general function to assemble the mass matrix for a unit cube. Note that we use `dolfinx.fem.form` to compile the variational form. For codes using `dolfinx.fem.petsc.LinearProblem`, you can supply `jit_options` as a keyword argument." + "Next we generate a general function to assemble the mass matrix for a unit cube. Note that we use `dolfinx.fem.form` to compile the variational form.\n", + "For codes using `dolfinx.fem.petsc.LinearProblem`, you can supply `jit_options` as a keyword argument." ] }, { @@ -136,8 +135,11 @@ " cffi_options = [option, \"-march=native\"]\n", " else:\n", " cffi_options = [option]\n", - " jit_options = {\"cffi_extra_compile_args\": cffi_options,\n", - " \"cache_dir\": cache_dir, \"cffi_libraries\": [\"m\"]}\n", + " jit_options = {\n", + " \"cffi_extra_compile_args\": cffi_options,\n", + " \"cache_dir\": cache_dir,\n", + " \"cffi_libraries\": [\"m\"],\n", + " }\n", " runtime = compile_form(space, degree, jit_options=jit_options)\n", " results[\"Space\"].append(space)\n", " results[\"Degree\"].append(str(degree))\n", @@ -150,7 +152,9 @@ "id": "10", "metadata": {}, "source": [ - "We have now stored all the results to a dictionary. To visualize it, we use pandas and its Dataframe class. We can inspect the data in a jupyter notebook as follows" + "We have now stored all the results to a dictionary. To visualize it, we use pandas and its Dataframe class.\n", + "To instpect the data in a Jupyter notebook, call the code below.\n", + "If you are running this code in a script, you can use `print(results_df)` instead." ] }, { @@ -169,7 +173,8 @@ "id": "12", "metadata": {}, "source": [ - "We can now make a plot for each element type to see the variation given the different compile options. We create a new colum for each element type and degree." + "Next, we inspect the impact of the compiler option on each type of finite element family.\n", + "To achieve this, we add an extra column to the dataframe, which combines the space and degree of the finite element." ] }, { @@ -195,7 +200,8 @@ "id": "14", "metadata": {}, "source": [ - "We observe that the compile time increases when increasing the degree of the function space, and that we get most speedup by using \"-O3\" or \"-Ofast\" combined with \"-march=native\"." + "We observe that the compile time increases when increasing the degree of the function space,\n", + "and that we get most speedup by using \"-O3\" or \"-Ofast\" combined with \"-march=native\"." ] } ], diff --git a/chapter4/compiler_parameters.py b/chapter4/compiler_parameters.py index ff20725f..d497aed3 100644 --- a/chapter4/compiler_parameters.py +++ b/chapter4/compiler_parameters.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -27,11 +27,9 @@ # We start by specifying the current directory as the location to place the generated C files, we obtain the current directory using pathlib # + -import matplotlib.pyplot as plt import pandas as pd import seaborn import time -import ufl from ufl import TestFunction, TrialFunction, dx, inner from dolfinx.mesh import create_unit_cube @@ -46,7 +44,8 @@ print(f"Directory to put C files in: {cache_dir}") # - -# Next we generate a general function to assemble the mass matrix for a unit cube. Note that we use `dolfinx.fem.form` to compile the variational form. For codes using `dolfinx.fem.petsc.LinearProblem`, you can supply `jit_options` as a keyword argument. +# Next we generate a general function to assemble the mass matrix for a unit cube. Note that we use `dolfinx.fem.form` to compile the variational form. +# For codes using `dolfinx.fem.petsc.LinearProblem`, you can supply `jit_options` as a keyword argument. # + @@ -86,20 +85,26 @@ def compile_form(space: str, degree: int, jit_options: Dict): cffi_options = [option, "-march=native"] else: cffi_options = [option] - jit_options = {"cffi_extra_compile_args": cffi_options, - "cache_dir": cache_dir, "cffi_libraries": ["m"]} + jit_options = { + "cffi_extra_compile_args": cffi_options, + "cache_dir": cache_dir, + "cffi_libraries": ["m"], + } runtime = compile_form(space, degree, jit_options=jit_options) results["Space"].append(space) results["Degree"].append(str(degree)) results["Options"].append("\n".join(cffi_options)) results["Time"].append(runtime) -# We have now stored all the results to a dictionary. To visualize it, we use pandas and its Dataframe class. We can inspect the data in a jupyter notebook as follows +# We have now stored all the results to a dictionary. To visualize it, we use pandas and its Dataframe class. +# To instpect the data in a Jupyter notebook, call the code below. +# If you are running this code in a script, you can use `print(results_df)` instead. results_df = pd.DataFrame.from_dict(results) results_df -# We can now make a plot for each element type to see the variation given the different compile options. We create a new colum for each element type and degree. +# Next, we inspect the impact of the compiler option on each type of finite element family. +# To achieve this, we add an extra column to the dataframe, which combines the space and degree of the finite element. seaborn.set(style="ticks") seaborn.set(font_scale=1.2) @@ -111,4 +116,5 @@ def compile_form(space: str, degree: int, jit_options: Dict): g = seaborn.catplot(x="Options", y="Time", kind="bar", data=df_e, col="Element") g.fig.set_size_inches(16, 4) -# We observe that the compile time increases when increasing the degree of the function space, and that we get most speedup by using "-O3" or "-Ofast" combined with "-march=native". +# We observe that the compile time increases when increasing the degree of the function space, +# and that we get most speedup by using "-O3" or "-Ofast" combined with "-march=native". diff --git a/chapter4/convergence.ipynb b/chapter4/convergence.ipynb index cf0babe4..3e457418 100644 --- a/chapter4/convergence.ipynb +++ b/chapter4/convergence.ipynb @@ -27,13 +27,29 @@ "outputs": [], "source": [ "from dolfinx import default_scalar_type\n", - "from dolfinx.fem import (Expression, Function, functionspace,\n", - " assemble_scalar, dirichletbc, form, locate_dofs_topological)\n", + "from dolfinx.fem import (\n", + " Expression,\n", + " Function,\n", + " functionspace,\n", + " assemble_scalar,\n", + " dirichletbc,\n", + " form,\n", + " locate_dofs_topological,\n", + ")\n", "from dolfinx.fem.petsc import LinearProblem\n", "from dolfinx.mesh import create_unit_square, locate_entities_boundary\n", "\n", "from mpi4py import MPI\n", - "from ufl import SpatialCoordinate, TestFunction, TrialFunction, div, dot, dx, grad, inner\n", + "from ufl import (\n", + " SpatialCoordinate,\n", + " TestFunction,\n", + " TrialFunction,\n", + " div,\n", + " dot,\n", + " dx,\n", + " grad,\n", + " inner,\n", + ")\n", "\n", "import ufl\n", "import numpy as np\n", @@ -48,7 +64,6 @@ "\n", "\n", "def solve_poisson(N=10, degree=1):\n", - "\n", " mesh = create_unit_square(MPI.COMM_WORLD, N, N)\n", " x = SpatialCoordinate(mesh)\n", " f = -div(grad(u_ufl(x)))\n", @@ -59,10 +74,18 @@ " L = f * v * dx\n", " u_bc = Function(V)\n", " u_bc.interpolate(u_numpy)\n", - " facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, lambda x: np.full(x.shape[1], True))\n", + " facets = locate_entities_boundary(\n", + " mesh, mesh.topology.dim - 1, lambda x: np.full(x.shape[1], True)\n", + " )\n", " dofs = locate_dofs_topological(V, mesh.topology.dim - 1, facets)\n", " bcs = [dirichletbc(u_bc, dofs)]\n", - " default_problem = LinearProblem(a, L, bcs=bcs, petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + " default_problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"poisson_convergence_\",\n", + " )\n", " return default_problem.solve(), u_ufl(x)" ] }, @@ -81,7 +104,7 @@ "source": [ "uh, u_ex = solve_poisson(10)\n", "comm = uh.function_space.mesh.comm\n", - "error = form((uh - u_ex)**2 * ufl.dx)\n", + "error = form((uh - u_ex) ** 2 * ufl.dx)\n", "E = np.sqrt(comm.allreduce(assemble_scalar(error), MPI.SUM))\n", "if comm.rank == 0:\n", " print(f\"L2-error: {E:.2e}\")" @@ -97,7 +120,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "eh = uh - u_ex\n", @@ -109,7 +134,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "### Reliable error norm computation\n", "However, as this gets expanded to `u_ex**2 + uh**2 - 2*u_ex*uh`. If the error is small, (and the solution itself is of moderate size), this calculation will correspond to subtract two positive numbers `u_ex**2 + uh**2`$\\sim 1$ and `2*u_ex*u`$\\sim 1$ yielding a small number, prone to round-off errors.\n", @@ -177,7 +204,7 @@ " # For L2 error estimations it is reccommended to send in u_numpy\n", " # as no JIT compilation is required\n", " Es[i] = error_L2(uh, u_numpy)\n", - " hs[i] = 1. / Ns[i]\n", + " hs[i] = 1.0 / Ns[i]\n", " if comm.rank == 0:\n", " print(f\"h: {hs[i]:.2e} Error: {Es[i]:.2e}\")" ] @@ -214,7 +241,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "degrees = [1, 2, 3, 4]\n", @@ -225,7 +254,7 @@ " uh, u_ex = solve_poisson(N, degree=degree)\n", " comm = uh.function_space.mesh.comm\n", " Es[i] = error_L2(uh, u_numpy, degree_raise=3)\n", - " hs[i] = 1. / Ns[i]\n", + " hs[i] = 1.0 / Ns[i]\n", " if comm.rank == 0:\n", " print(f\"h: {hs[i]:.2e} Error: {Es[i]:.2e}\")\n", " rates = np.log(Es[1:] / Es[:-1]) / np.log(hs[1:] / hs[:-1])\n", @@ -235,7 +264,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "### Infinity norm estimates\n", "We start by creating a function to compute the infinity norm, the max difference between the approximate and exact solution." @@ -284,7 +315,7 @@ " uh, u_ex = solve_poisson(N, degree=degree)\n", " comm = uh.function_space.mesh.comm\n", " Es[i] = error_infinity(uh, u_numpy)\n", - " hs[i] = 1. / Ns[i]\n", + " hs[i] = 1.0 / Ns[i]\n", " if comm.rank == 0:\n", " print(f\"h: {hs[i]:.2e} Error: {Es[i]:.2e}\")\n", " rates = np.log(Es[1:] / Es[:-1]) / np.log(hs[1:] / hs[:-1])\n", diff --git a/chapter4/convergence.py b/chapter4/convergence.py index dfa22927..da8b9bae 100644 --- a/chapter4/convergence.py +++ b/chapter4/convergence.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -26,13 +26,29 @@ # + from dolfinx import default_scalar_type -from dolfinx.fem import (Expression, Function, functionspace, - assemble_scalar, dirichletbc, form, locate_dofs_topological) +from dolfinx.fem import ( + Expression, + Function, + functionspace, + assemble_scalar, + dirichletbc, + form, + locate_dofs_topological, +) from dolfinx.fem.petsc import LinearProblem from dolfinx.mesh import create_unit_square, locate_entities_boundary from mpi4py import MPI -from ufl import SpatialCoordinate, TestFunction, TrialFunction, div, dot, dx, grad, inner +from ufl import ( + SpatialCoordinate, + TestFunction, + TrialFunction, + div, + dot, + dx, + grad, + inner, +) import ufl import numpy as np @@ -47,7 +63,6 @@ def u_ex(mod): def solve_poisson(N=10, degree=1): - mesh = create_unit_square(MPI.COMM_WORLD, N, N) x = SpatialCoordinate(mesh) f = -div(grad(u_ufl(x))) @@ -58,10 +73,18 @@ def solve_poisson(N=10, degree=1): L = f * v * dx u_bc = Function(V) u_bc.interpolate(u_numpy) - facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, lambda x: np.full(x.shape[1], True)) + facets = locate_entities_boundary( + mesh, mesh.topology.dim - 1, lambda x: np.full(x.shape[1], True) + ) dofs = locate_dofs_topological(V, mesh.topology.dim - 1, facets) bcs = [dirichletbc(u_bc, dofs)] - default_problem = LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) + default_problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="poisson_convergence_", + ) return default_problem.solve(), u_ufl(x) @@ -71,7 +94,7 @@ def solve_poisson(N=10, degree=1): uh, u_ex = solve_poisson(10) comm = uh.function_space.mesh.comm -error = form((uh - u_ex)**2 * ufl.dx) +error = form((uh - u_ex) ** 2 * ufl.dx) E = np.sqrt(comm.allreduce(assemble_scalar(error), MPI.SUM)) if comm.rank == 0: print(f"L2-error: {E:.2e}") @@ -90,6 +113,7 @@ def solve_poisson(N=10, degree=1): # # To avoid this issue, we interpolate the approximate and exact solution into a higher order function space. Then we subtract the degrees of freedom from the interpolated functions to create a new error function. Then, finally, we assemble/integrate the square difference and take the square root to get the L2 norm. + def error_L2(uh, u_ex, degree_raise=3): # Create higher order function space degree = uh.function_space.ufl_element().degree @@ -133,7 +157,7 @@ def error_L2(uh, u_ex, degree_raise=3): # For L2 error estimations it is reccommended to send in u_numpy # as no JIT compilation is required Es[i] = error_L2(uh, u_numpy) - hs[i] = 1. / Ns[i] + hs[i] = 1.0 / Ns[i] if comm.rank == 0: print(f"h: {hs[i]:.2e} Error: {Es[i]:.2e}") @@ -157,7 +181,7 @@ def error_L2(uh, u_ex, degree_raise=3): uh, u_ex = solve_poisson(N, degree=degree) comm = uh.function_space.mesh.comm Es[i] = error_L2(uh, u_numpy, degree_raise=3) - hs[i] = 1. / Ns[i] + hs[i] = 1.0 / Ns[i] if comm.rank == 0: print(f"h: {hs[i]:.2e} Error: {Es[i]:.2e}") rates = np.log(Es[1:] / Es[:-1]) / np.log(hs[1:] / hs[:-1]) @@ -168,6 +192,7 @@ def error_L2(uh, u_ex, degree_raise=3): # ### Infinity norm estimates # We start by creating a function to compute the infinity norm, the max difference between the approximate and exact solution. + def error_infinity(u_h, u_ex): # Interpolate exact solution, special handling if exact solution # is a ufl expression or a python lambda function @@ -194,7 +219,7 @@ def error_infinity(u_h, u_ex): uh, u_ex = solve_poisson(N, degree=degree) comm = uh.function_space.mesh.comm Es[i] = error_infinity(uh, u_numpy) - hs[i] = 1. / Ns[i] + hs[i] = 1.0 / Ns[i] if comm.rank == 0: print(f"h: {hs[i]:.2e} Error: {Es[i]:.2e}") rates = np.log(Es[1:] / Es[:-1]) / np.log(hs[1:] / hs[:-1]) diff --git a/chapter4/newton-solver.ipynb b/chapter4/newton-solver.ipynb index 01c1ee34..823b6021 100644 --- a/chapter4/newton-solver.ipynb +++ b/chapter4/newton-solver.ipynb @@ -42,7 +42,9 @@ "cell_type": "code", "execution_count": null, "id": "2", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "import dolfinx\n", @@ -58,7 +60,9 @@ { "cell_type": "markdown", "id": "3", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "We will consider the following non-linear problem:\n", "\n", @@ -126,7 +130,7 @@ "source": [ "v = ufl.TestFunction(V)\n", "x = ufl.SpatialCoordinate(mesh)\n", - "F = uh**2 * v * ufl.dx - 2 * uh * v * ufl.dx - (x[0]**2 + 4 * x[0] + 3) * v * ufl.dx\n", + "F = uh**2 * v * ufl.dx - 2 * uh * v * ufl.dx - (x[0] ** 2 + 4 * x[0] + 3) * v * ufl.dx\n", "residual = dolfinx.fem.form(F)" ] }, @@ -217,7 +221,8 @@ "id": "17", "metadata": {}, "source": [ - "We are now ready to solve the linear problem. At each iteration, we reassemble the Jacobian and residual, and use the norm of the magnitude of the update (`dx`) as a termination criteria.\n", + "We are now ready to solve the linear problem.\n", + "At each iteration, we reassemble the Jacobian and residual, and use the norm of the magnitude of the update (`dx`) as a termination criteria.\n", "## The Newton iterations" ] }, @@ -225,7 +230,9 @@ "cell_type": "code", "execution_count": null, "id": "18", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 0 + }, "outputs": [], "source": [ "i = 0\n", @@ -255,7 +262,7 @@ " print(f\"Iteration {i}: Correction norm {correction_norm}\")\n", " if correction_norm < 1e-10:\n", " break\n", - " solutions[i, :] = uh.x.array[sort_order]" + " solutions[i, :] = uh.x.array[sort_order]\n" ] }, { @@ -274,7 +281,10 @@ "outputs": [], "source": [ "dolfinx.fem.petsc.assemble_vector(L, residual)\n", - "print(f\"Final residual {L.norm(0)}\")" + "print(f\"Final residual {L.norm(0)}\")\n", + "A.destroy()\n", + "L.destroy()\n", + "solver.destroy()" ] }, { @@ -290,7 +300,9 @@ "cell_type": "code", "execution_count": null, "id": "22", - "metadata": {}, + "metadata": { + "lines_to_end_of_cell_marker": 2 + }, "outputs": [], "source": [ "# Plot solution for each of the iterations\n", @@ -315,7 +327,9 @@ { "cell_type": "markdown", "id": "23", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "# Newton's method with DirichletBC\n", "In the previous example, we did not consider handling of Dirichlet boundary conditions.\n", @@ -337,7 +351,7 @@ "domain = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)\n", "x = ufl.SpatialCoordinate(domain)\n", "u_ufl = 1 + x[0] + 2 * x[1]\n", - "f = - ufl.div(q(u_ufl) * ufl.grad(u_ufl))\n", + "f = -ufl.div(q(u_ufl) * ufl.grad(u_ufl))\n", "\n", "\n", "def u_exact(x):\n", @@ -367,7 +381,9 @@ "fdim = domain.topology.dim - 1\n", "domain.topology.create_connectivity(fdim, fdim + 1)\n", "boundary_facets = dolfinx.mesh.exterior_facet_indices(domain.topology)\n", - "bc = dolfinx.fem.dirichletbc(u_D, dolfinx.fem.locate_dofs_topological(V, fdim, boundary_facets))\n", + "bc = dolfinx.fem.dirichletbc(\n", + " u_D, dolfinx.fem.locate_dofs_topological(V, fdim, boundary_facets)\n", + ")\n", "\n", "uh = dolfinx.fem.Function(V)\n", "v = ufl.TestFunction(V)\n", @@ -434,7 +450,9 @@ "outputs": [], "source": [ "i = 0\n", - "error = dolfinx.fem.form(ufl.inner(uh - u_ufl, uh - u_ufl) * ufl.dx(metadata={\"quadrature_degree\": 4}))\n", + "error = dolfinx.fem.form(\n", + " ufl.inner(uh - u_ufl, uh - u_ufl) * ufl.dx(metadata={\"quadrature_degree\": 4})\n", + ")\n", "L2_error = []\n", "du_norm = []\n", "while i < max_iterations:\n", @@ -466,7 +484,9 @@ " correction_norm = du.x.petsc_vec.norm(0)\n", "\n", " # Compute L2 error comparing to the analytical solution\n", - " L2_error.append(np.sqrt(mesh.comm.allreduce(dolfinx.fem.assemble_scalar(error), op=MPI.SUM)))\n", + " L2_error.append(\n", + " np.sqrt(mesh.comm.allreduce(dolfinx.fem.assemble_scalar(error), op=MPI.SUM))\n", + " )\n", " du_norm.append(correction_norm)\n", "\n", " print(f\"Iteration {i}: Correction norm {correction_norm}, L2 error: {L2_error[-1]}\")\n", @@ -494,7 +514,7 @@ "plt.plot(np.arange(i), L2_error)\n", "plt.title(r\"$L^2(\\Omega)$-error of $u_h$\")\n", "ax = plt.gca()\n", - "ax.set_yscale('log')\n", + "ax.set_yscale(\"log\")\n", "plt.xlabel(\"Iterations\")\n", "plt.ylabel(r\"$L^2$-error\")\n", "plt.grid()\n", @@ -502,7 +522,7 @@ "plt.title(r\"Residual of $\\vert\\vert\\delta u_i\\vert\\vert$\")\n", "plt.plot(np.arange(i), du_norm)\n", "ax = plt.gca()\n", - "ax.set_yscale('log')\n", + "ax.set_yscale(\"log\")\n", "plt.xlabel(\"Iterations\")\n", "plt.ylabel(r\"$\\vert\\vert \\delta u\\vert\\vert$\")\n", "plt.grid()" @@ -535,7 +555,7 @@ "metadata": {}, "outputs": [], "source": [ - "pyvista.start_xvfb()\n", + "pyvista.start_xvfb(1.0)\n", "u_topology, u_cell_types, u_geometry = dolfinx.plot.vtk_mesh(V)\n", "u_grid = pyvista.UnstructuredGrid(u_topology, u_cell_types, u_geometry)\n", "u_grid.point_data[\"u\"] = uh.x.array.real\n", @@ -546,14 +566,6 @@ "if not pyvista.OFF_SCREEN:\n", " u_plotter.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "36", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/chapter4/newton-solver.py b/chapter4/newton-solver.py index 8c2f5e2f..312cbebc 100644 --- a/chapter4/newton-solver.py +++ b/chapter4/newton-solver.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -55,6 +55,7 @@ # For this problem, we have two solutions, $u=-x-1$, $u=x+3$. # We define these roots as python functions, and create an appropriate spacing for plotting these soultions. + # + def root_0(x): return 3 + x[0] @@ -81,7 +82,7 @@ def root_1(x): v = ufl.TestFunction(V) x = ufl.SpatialCoordinate(mesh) -F = uh**2 * v * ufl.dx - 2 * uh * v * ufl.dx - (x[0]**2 + 4 * x[0] + 3) * v * ufl.dx +F = uh**2 * v * ufl.dx - 2 * uh * v * ufl.dx - (x[0] ** 2 + 4 * x[0] + 3) * v * ufl.dx residual = dolfinx.fem.form(F) # Next, we can define the jacobian $J_F$, by using `ufl.derivative`. @@ -110,7 +111,8 @@ def root_1(x): solutions = np.zeros((max_iterations + 1, len(coords))) solutions[0] = uh.x.array[sort_order] -# We are now ready to solve the linear problem. At each iteration, we reassemble the Jacobian and residual, and use the norm of the magnitude of the update (`dx`) as a termination criteria. +# We are now ready to solve the linear problem. +# At each iteration, we reassemble the Jacobian and residual, and use the norm of the magnitude of the update (`dx`) as a termination criteria. # ## The Newton iterations i = 0 @@ -146,6 +148,9 @@ def root_1(x): dolfinx.fem.petsc.assemble_vector(L, residual) print(f"Final residual {L.norm(0)}") +A.destroy() +L.destroy() +solver.destroy() # ## Visualization of Newton iterations # We next look at the evolution of the solution and the error of the solution when compared to the two exact roots of the problem. @@ -177,6 +182,7 @@ def root_1(x): # For this example, we will consider the [non-linear Poisson](./../chapter2/nonlinpoisson)-problem. # We start by defining the mesh, the analytical solution and the forcing term $f$. + # + def q(u): return 1 + u**2 @@ -185,7 +191,7 @@ def q(u): domain = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10) x = ufl.SpatialCoordinate(domain) u_ufl = 1 + x[0] + 2 * x[1] -f = - ufl.div(q(u_ufl) * ufl.grad(u_ufl)) +f = -ufl.div(q(u_ufl) * ufl.grad(u_ufl)) def u_exact(x): @@ -203,7 +209,9 @@ def u_exact(x): fdim = domain.topology.dim - 1 domain.topology.create_connectivity(fdim, fdim + 1) boundary_facets = dolfinx.mesh.exterior_facet_indices(domain.topology) -bc = dolfinx.fem.dirichletbc(u_D, dolfinx.fem.locate_dofs_topological(V, fdim, boundary_facets)) +bc = dolfinx.fem.dirichletbc( + u_D, dolfinx.fem.locate_dofs_topological(V, fdim, boundary_facets) +) uh = dolfinx.fem.Function(V) v = ufl.TestFunction(V) @@ -242,7 +250,9 @@ def u_exact(x): i = 0 -error = dolfinx.fem.form(ufl.inner(uh - u_ufl, uh - u_ufl) * ufl.dx(metadata={"quadrature_degree": 4})) +error = dolfinx.fem.form( + ufl.inner(uh - u_ufl, uh - u_ufl) * ufl.dx(metadata={"quadrature_degree": 4}) +) L2_error = [] du_norm = [] while i < max_iterations: @@ -274,7 +284,9 @@ def u_exact(x): correction_norm = du.x.petsc_vec.norm(0) # Compute L2 error comparing to the analytical solution - L2_error.append(np.sqrt(mesh.comm.allreduce(dolfinx.fem.assemble_scalar(error), op=MPI.SUM))) + L2_error.append( + np.sqrt(mesh.comm.allreduce(dolfinx.fem.assemble_scalar(error), op=MPI.SUM)) + ) du_norm.append(correction_norm) print(f"Iteration {i}: Correction norm {correction_norm}, L2 error: {L2_error[-1]}") @@ -288,7 +300,7 @@ def u_exact(x): plt.plot(np.arange(i), L2_error) plt.title(r"$L^2(\Omega)$-error of $u_h$") ax = plt.gca() -ax.set_yscale('log') +ax.set_yscale("log") plt.xlabel("Iterations") plt.ylabel(r"$L^2$-error") plt.grid() @@ -296,7 +308,7 @@ def u_exact(x): plt.title(r"Residual of $\vert\vert\delta u_i\vert\vert$") plt.plot(np.arange(i), du_norm) ax = plt.gca() -ax.set_yscale('log') +ax.set_yscale("log") plt.xlabel("Iterations") plt.ylabel(r"$\vert\vert \delta u\vert\vert$") plt.grid() @@ -307,7 +319,7 @@ def u_exact(x): if domain.comm.rank == 0: print(f"Error_max: {error_max:.2e}") -pyvista.start_xvfb() +pyvista.start_xvfb(1.0) u_topology, u_cell_types, u_geometry = dolfinx.plot.vtk_mesh(V) u_grid = pyvista.UnstructuredGrid(u_topology, u_cell_types, u_geometry) u_grid.point_data["u"] = uh.x.array.real @@ -317,5 +329,3 @@ def u_exact(x): u_plotter.view_xy() if not pyvista.OFF_SCREEN: u_plotter.show() - - diff --git a/chapter4/solvers.ipynb b/chapter4/solvers.ipynb index 4322a7a2..8ac67b38 100644 --- a/chapter4/solvers.ipynb +++ b/chapter4/solvers.ipynb @@ -96,7 +96,9 @@ "L = f * v * dx\n", "u_bc = Function(V)\n", "u_bc.interpolate(u_numpy)\n", - "facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, lambda x: numpy.full(x.shape[1], True))\n", + "facets = locate_entities_boundary(\n", + " mesh, mesh.topology.dim - 1, lambda x: numpy.full(x.shape[1], True)\n", + ")\n", "dofs = locate_dofs_topological(V, mesh.topology.dim - 1, facets)\n", "bcs = [dirichletbc(u_bc, dofs)]" ] @@ -114,8 +116,13 @@ "metadata": {}, "outputs": [], "source": [ - "default_problem = LinearProblem(a, L, bcs=bcs,\n", - " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"})\n", + "default_problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\"ksp_type\": \"preonly\", \"pc_type\": \"lu\"},\n", + " petsc_options_prefix=\"poisson_l\",\n", + ")\n", "uh = default_problem.solve()" ] }, @@ -158,8 +165,18 @@ "metadata": {}, "outputs": [], "source": [ - "cg_problem = LinearProblem(a, L, bcs=bcs,\n", - " petsc_options={\"ksp_type\": \"cg\", \"ksp_rtol\": 1e-6, \"ksp_atol\": 1e-10, \"ksp_max_it\": 1000})\n", + "cg_problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\n", + " \"ksp_type\": \"cg\",\n", + " \"ksp_rtol\": 1e-6,\n", + " \"ksp_atol\": 1e-10,\n", + " \"ksp_max_it\": 1000,\n", + " },\n", + " petsc_options_prefix=\"poisson_cg_\",\n", + ")\n", "uh = cg_problem.solve()\n", "cg_solver = cg_problem.solver\n", "viewer = PETSc.Viewer().createASCII(\"cg_output.txt\")\n", @@ -182,8 +199,19 @@ "metadata": {}, "outputs": [], "source": [ - "gmres_problem = LinearProblem(a, L, bcs=bcs,\n", - " petsc_options={\"ksp_type\": \"gmres\", \"ksp_rtol\": 1e-6, \"ksp_atol\": 1e-10, \"ksp_max_it\": 1000, \"pc_type\": \"none\"})\n", + "gmres_problem = LinearProblem(\n", + " a,\n", + " L,\n", + " bcs=bcs,\n", + " petsc_options={\n", + " \"ksp_type\": \"gmres\",\n", + " \"ksp_rtol\": 1e-6,\n", + " \"ksp_atol\": 1e-10,\n", + " \"ksp_max_it\": 1000,\n", + " \"pc_type\": \"none\",\n", + " },\n", + " petsc_options_prefix=\"poisson_gmres_\",\n", + ")\n", "uh = gmres_problem.solve()\n", "gmres_solver = gmres_problem.solver\n", "viewer = PETSc.Viewer().createASCII(\"gmres_output.txt\")\n", diff --git a/chapter4/solvers.py b/chapter4/solvers.py index ceee9393..f2d27eed 100644 --- a/chapter4/solvers.py +++ b/chapter4/solvers.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.16.5 +# jupytext_version: 1.17.2 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -66,14 +66,21 @@ def u_ex(mod): L = f * v * dx u_bc = Function(V) u_bc.interpolate(u_numpy) -facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, lambda x: numpy.full(x.shape[1], True)) +facets = locate_entities_boundary( + mesh, mesh.topology.dim - 1, lambda x: numpy.full(x.shape[1], True) +) dofs = locate_dofs_topological(V, mesh.topology.dim - 1, facets) bcs = [dirichletbc(u_bc, dofs)] # We start by solving the problem with an LU factorization, a direct solver method (similar to Gaussian elimination). -default_problem = LinearProblem(a, L, bcs=bcs, - petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) +default_problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={"ksp_type": "preonly", "pc_type": "lu"}, + petsc_options_prefix="poisson_l", +) uh = default_problem.solve() # We now look at the solver process by inspecting the `PETSc`-solver. As the view-options in PETSc are not adjusted for notebooks (`solver.view()` will print output to the terminal if used in a `.py` file), we write the solver output to file and read it in and print the output. @@ -92,8 +99,18 @@ def u_ex(mod): # As the Poisson equation results in a symmetric, positive definite system matrix, the optimal Krylov solver is the conjugate gradient (Lagrange) method. The default preconditioner is the incomplete LU factorization (ILU), which is a popular and robust overall preconditioner. We can change the preconditioner by setting `"pc_type"` to some of the other preconditioners in petsc, which you can find at [PETSc KSP solvers](https://petsc.org/release/manual/ksp/#tab-kspdefaults) and [PETSc preconditioners](https://petsc.org/release/manual/ksp/#tab-pcdefaults). # You can set any option in `PETSc` through the `petsc_options` input, such as the absolute tolerance (`"ksp_atol"`), relative tolerance (`"ksp_rtol"`) and maximum number of iterations (`"ksp_max_it"`). -cg_problem = LinearProblem(a, L, bcs=bcs, - petsc_options={"ksp_type": "cg", "ksp_rtol": 1e-6, "ksp_atol": 1e-10, "ksp_max_it": 1000}) +cg_problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={ + "ksp_type": "cg", + "ksp_rtol": 1e-6, + "ksp_atol": 1e-10, + "ksp_max_it": 1000, + }, + petsc_options_prefix="poisson_cg_", +) uh = cg_problem.solve() cg_solver = cg_problem.solver viewer = PETSc.Viewer().createASCII("cg_output.txt") @@ -104,8 +121,19 @@ def u_ex(mod): # For non-symmetric problems, a Krylov solver for non-symmetric systems, such as GMRES is better. -gmres_problem = LinearProblem(a, L, bcs=bcs, - petsc_options={"ksp_type": "gmres", "ksp_rtol": 1e-6, "ksp_atol": 1e-10, "ksp_max_it": 1000, "pc_type": "none"}) +gmres_problem = LinearProblem( + a, + L, + bcs=bcs, + petsc_options={ + "ksp_type": "gmres", + "ksp_rtol": 1e-6, + "ksp_atol": 1e-10, + "ksp_max_it": 1000, + "pc_type": "none", + }, + petsc_options_prefix="poisson_gmres_", +) uh = gmres_problem.solve() gmres_solver = gmres_problem.solver viewer = PETSc.Viewer().createASCII("gmres_output.txt")