Buckets:
| # --- | |
| # jupyter: | |
| # jupytext: | |
| # formats: ipynb,py:light | |
| # text_representation: | |
| # extension: .py | |
| # format_name: light | |
| # format_version: '1.5' | |
| # jupytext_version: 1.18.1 | |
| # kernelspec: | |
| # display_name: Python 3 (ipykernel) | |
| # language: python | |
| # name: python3 | |
| # --- | |
| # # Implementation | |
| # Author: Jørgen S. Dokken | |
| # | |
| # In this section, we will solve the deflection of the membrane problem. | |
| # After finishing this section, you should be able to: | |
| # - Create a simple mesh using the GMSH Python API and load it into DOLFINx | |
| # - Create constant boundary conditions using a {py:func}`geometrical identifier<dolfinx.fem.locate_dofs_geometrical>` | |
| # - Use {py:class}`ufl.SpatialCoordinate` to create a spatially varying function | |
| # - Interpolate a {py:class}`ufl-Expression<ufl.core.expr.Expr>` into an appropriate function space | |
| # - Evaluate a {py:class}`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. | |
| membrane = gmsh.model.occ.addDisk(0, 0, 0, 1, 1) | |
| gmsh.model.occ.synchronize() | |
| # After that, we make the membrane a physical surface, such that it is recognized by `gmsh` when generating the mesh. | |
| # As a surface is a two-dimensional entity, we add `2` as the first argument, | |
| # the entity tag of the membrane as the second argument, and the physical tag as the last argument. | |
| # In a later demo, we will get into when this tag matters. | |
| gdim = 2 | |
| gmsh.model.addPhysicalGroup(gdim, [membrane], 1) | |
| # Finally, we generate the two-dimensional mesh. | |
| # We set a uniform mesh size by modifying the GMSH options. | |
| gmsh.option.setNumber("Mesh.CharacteristicLengthMin", 0.05) | |
| gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.05) | |
| gmsh.model.mesh.generate(gdim) | |
| # # Interfacing with GMSH in DOLFINx | |
| # We will import the GMSH-mesh directly from GMSH into DOLFINx via the {py:mod}`dolfinx.io.gmsh` interface. | |
| # The {py:mod}`dolfinx.io.gmsh` module contains two functions | |
| # 1. {py:func}`model_to_mesh<dolfinx.io.gmsh.model_to_mesh>` which takes in a `gmsh.model` | |
| # and returns a {py:class}`dolfinx.io.gmsh.MeshData` object. | |
| # 2. {py:func}`read_from_msh<dolfinx.io.gmsh.read_from_msh>` which takes in a path to a `.msh`-file | |
| # and returns a {py:class}`dolfinx.io.gmsh.MeshData` object. | |
| # | |
| # The {py:class}`MeshData` object will contain a {py:class}`dolfinx.mesh.Mesh`, | |
| # under the attribute {py:attr}`mesh<dolfinx.io.gmsh.MeshData.mesh>`. | |
| # This mesh will contain all GMSH Physical Groups of the highest topological dimension. | |
| # ```{note} | |
| # If you do not use `gmsh.model.addPhysicalGroup` when creating the mesh with GMSH, it can not be read into DOLFINx. | |
| # ``` | |
| # The {py:class}`MeshData<dolfinx.io.gmsh.MeshData>` object can also contain tags for | |
| # all other `PhysicalGroups` that has been added to the mesh, that being | |
| # {py:attr}`cell_tags<dolfinx.io.gmsh.MeshData.cell_tags>`, {py:attr}`facet_tags<dolfinx.io.gmsh.MeshData.facet_tags>`, | |
| # {py:attr}`ridge_tags<dolfinx.io.gmsh.MeshData.ridge_tags>` and | |
| # {py:attr}`peak_tags<dolfinx.io.gmsh.MeshData.peak_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 {py:func}`model_to_mesh<dolfinx.io.gmsh.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 gmsh as gmshio | |
| from dolfinx.fem.petsc import LinearProblem | |
| from mpi4py import MPI | |
| gmsh_model_rank = 0 | |
| mesh_comm = MPI.COMM_WORLD | |
| mesh_data = gmshio.model_to_mesh(gmsh.model, mesh_comm, gmsh_model_rank, gdim=gdim) | |
| assert mesh_data.cell_tags is not None | |
| cell_markers = mesh_data.cell_tags | |
| domain = mesh_data.mesh | |
| # - | |
| # 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 {py:class}`ufl.SpatialCoordinate` and two constants, | |
| # one for $\beta$ and one for $R_0$. | |
| # + | |
| import ufl | |
| from dolfinx import default_scalar_type | |
| x = ufl.SpatialCoordinate(domain) | |
| beta = fem.Constant(domain, default_scalar_type(12)) | |
| R0 = fem.Constant(domain, default_scalar_type(0.3)) | |
| p = 4 * ufl.exp(-(beta**2) * (x[0] ** 2 + (x[1] - R0) ** 2)) | |
| # - | |
| # ## Create a Dirichlet boundary condition using geometrical conditions | |
| # The next step is to create the homogeneous boundary condition. | |
| # As opposed to the [first tutorial](./fundamentals_code.ipynb) we will use | |
| # {py:func}`locate_dofs_geometrical<dolfinx.fem.locate_dofs_geometrical>` to locate the degrees of freedom on the boundary. | |
| # As we know that our domain is a circle with radius 1, we know that any degree of freedom should be | |
| # located at a coordinate $(x,y)$ such that $\sqrt{x^2+y^2}=1$. | |
| # + | |
| import numpy as np | |
| def on_boundary(x): | |
| return np.isclose(np.sqrt(x[0] ** 2 + x[1] ** 2), 1) | |
| boundary_dofs = fem.locate_dofs_geometrical(V, on_boundary) | |
| # - | |
| # As our Dirichlet condition is homogeneous (`u=0` on the whole boundary), we can initialize the | |
| # {py:class}`dolfinx.fem.DirichletBC` with a constant value, the degrees of freedom and the function | |
| # space to apply the boundary condition on. We use the constructor {py:func}`dolfinx.fem.dirichletbc`. | |
| bc = fem.dirichletbc(default_scalar_type(0), boundary_dofs, V) | |
| # ## Defining the variational problem | |
| # The variational problem is the same as in our first Poisson problem, where `f` is replaced by `p`. | |
| u = ufl.TrialFunction(V) | |
| v = ufl.TestFunction(V) | |
| 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"}, | |
| petsc_options_prefix="membrane_", | |
| ) | |
| uh = problem.solve() | |
| # ## Interpolation of a UFL-expression | |
| # As we previously defined the load `p` as a spatially varying function, | |
| # we would like to interpolate this function into an appropriate function space for visualization. | |
| # To do this we use the class {py:class}`Expression<dolfinx.fem.Expression>`. | |
| # The expression takes in any UFL-expression, and a set of points on the reference element. | |
| # We will use the {py:attr}`interpolation points<dolfinx.fem.FiniteElement.interpolation_points>` | |
| # of the space we want to interpolate in to. | |
| # We choose a high order function space to represent the function `p`, as it is rapidly varying in space. | |
| Q = fem.functionspace(domain, ("Lagrange", 5)) | |
| expr = fem.Expression(p, Q.element.interpolation_points) | |
| pressure = fem.Function(Q) | |
| pressure.interpolate(expr) | |
| # ## Plotting the solution over a line | |
| # We first plot the deflection $u_h$ over the domain $\Omega$. | |
| from dolfinx.plot import vtk_mesh | |
| import pyvista | |
| # Extract topology from mesh and create {py:class}`pyvista.UnstructuredGrid` | |
| 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) | |
| plotter = pyvista.Plotter() | |
| plotter.add_mesh(warped, show_edges=True, show_scalar_bar=True, scalars="u") | |
| if not pyvista.OFF_SCREEN: | |
| plotter.show() | |
| else: | |
| plotter.screenshot("deflection.png") | |
| # - | |
| # We next plot the load on the domain | |
| load_plotter = pyvista.Plotter() | |
| p_grid = pyvista.UnstructuredGrid(*vtk_mesh(Q)) | |
| p_grid.point_data["p"] = pressure.x.array.real | |
| warped_p = p_grid.warp_by_scalar("p", factor=0.5) | |
| warped_p.set_active_scalars("p") | |
| load_plotter.add_mesh(warped_p, show_scalar_bar=True) | |
| load_plotter.view_xy() | |
| if not pyvista.OFF_SCREEN: | |
| load_plotter.show() | |
| else: | |
| load_plotter.screenshot("load.png") | |
| # ## Making curve plots throughout the domain | |
| # Another way to compare the deflection and the load is to make a plot along the line $x=0$. | |
| # This is just a matter of defining a set of points along the $y$-axis and evaluating the | |
| # finite element functions $u$ and $p$ at these points. | |
| tol = 0.001 # Avoid hitting the outside of the domain | |
| y = np.linspace(-1 + tol, 1 - tol, 101) | |
| points = np.zeros((3, 101)) | |
| points[1] = y | |
| u_values = [] | |
| p_values = [] | |
| # As a finite element function is the linear combination of all degrees of freedom, | |
| # $u_h(x)=\sum_{i=1}^N c_i \phi_i(x)$ where $c_i$ are the coefficients of $u_h$ and $\phi_i$ | |
| # is the $i$-th basis function, we can compute the exact solution at any point in $\Omega$. | |
| # 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 {py:class}`bounding box tree<dolfinx.geometry.BoundingBoxTree` | |
| # 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 | |
| # {py:func}`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 | |
| # {py:class}`dolfinx.graph.AdjacencyList`, where one can access the cells for the | |
| # `i`th point by calling {py:meth}`links(i)<dolfinx.graph.AdjacencyList.links>`. | |
| # 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 using | |
| # {py:func}`dolfinx.geometry.compute_colliding_cells`, | |
| # which measures the exact distance between the point and the cell | |
| # (approximated as a convex hull for higher order geometries). | |
| # This function also returns an adjacency-list, as the point might align with a facet, | |
| # edge or vertex that is shared between multiple cells in the mesh. | |
| # | |
| # Finally, we would like the code below to run in parallel, | |
| # when the mesh is distributed over multiple processors. | |
| # In that case, it is not guaranteed that every point in `points` is on each processor. | |
| # Therefore we create a subset `points_on_proc` only containing the points found on the current processor. | |
| cells = [] | |
| points_on_proc = [] | |
| # Find cells whose bounding-box collide with the the points | |
| cell_candidates = geometry.compute_collisions_points(bb_tree, points.T) | |
| # Choose one of the cells that contains the point | |
| colliding_cells = geometry.compute_colliding_cells(domain, cell_candidates, points.T) | |
| for i, point in enumerate(points.T): | |
| if len(colliding_cells.links(i)) > 0: | |
| points_on_proc.append(point) | |
| cells.append(colliding_cells.links(i)[0]) | |
| # We now have a list of points on the processor, on in which cell each point belongs. | |
| # We can then call {py:meth}`uh.eval<dolfinx.fem.Function.eval>` and | |
| # {py:meth}`pressure.eval<dolfinx.fem.Function.eval>` to obtain the set of values for all the points. | |
| points_on_proc = np.array(points_on_proc, dtype=np.float64) | |
| u_values = uh.eval(points_on_proc, cells) | |
| p_values = pressure.eval(points_on_proc, cells) | |
| # As we now have an array of coordinates and two arrays of function values, | |
| # we can use {py:mod}`matplotlib<matplotlib.pyplot>` to plot them | |
| # + | |
| import matplotlib.pyplot as plt | |
| fig = plt.figure() | |
| plt.plot( | |
| points_on_proc[:, 1], | |
| 50 * u_values, | |
| "k", | |
| linewidth=2, | |
| label="Deflection ($\\times 50$)", | |
| ) | |
| plt.plot(points_on_proc[:, 1], p_values, "b--", linewidth=2, label="Load") | |
| plt.grid(True) | |
| plt.xlabel("y") | |
| plt.legend() | |
| # - | |
| # 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 | |
| pressure.name = "Load" | |
| uh.name = "Deflection" | |
| results_folder = Path("results") | |
| results_folder.mkdir(exist_ok=True, parents=True) | |
| with dolfinx.io.VTXWriter( | |
| MPI.COMM_WORLD, results_folder / "membrane_pressure.bp", [pressure], engine="BP4" | |
| ) as vtx: | |
| vtx.write(0.0) | |
| with dolfinx.io.VTXWriter( | |
| MPI.COMM_WORLD, results_folder / "membrane_deflection.bp", [uh], engine="BP4" | |
| ) as vtx: | |
| vtx.write(0.0) |
Xet Storage Details
- Size:
- 12.9 kB
- Xet hash:
- c2f888eedb5303e291b9dc7f979183791bc7d354ff0a7810dd48a86ce097ff4d
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.