Skip to content

Commit f00a2b9

Browse files
authored
Update membrane code with crosslinks (#279)
* Update membrane code with crosslinks * Ruff format * Remove debug print * Remove pritn * Finalize membrane * Last set of links + matplotlib intersphinx
1 parent 17d2f6b commit f00a2b9

24 files changed

+200
-128
lines changed

_config.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,20 @@ sphinx:
4242
# navigation_with_keys: false
4343
codeautolink_concat_default: True
4444
intersphinx_mapping:
45-
basix : ["https://docs.fenicsproject.org/basix/main/python/", null]
46-
ffcx : ["https://docs.fenicsproject.org/ffcx/main/", null]
47-
ufl : ["https://docs.fenicsproject.org/ufl/main/", null]
48-
dolfinx : ["https://docs.fenicsproject.org/dolfinx/main/python", null]
49-
petsc4py: ["https://petsc.org/release/petsc4py", null]
50-
mpi4py: ["https://mpi4py.readthedocs.io/en/stable", null]
51-
numpy: ["https://numpy.org/doc/stable/", null]
52-
pyvista: ["https://docs.pyvista.org/", null]
53-
packaging: ["https://packaging.pypa.io/en/stable/", null]
45+
basix: ["https://docs.fenicsproject.org/basix/main/python/", null]
46+
ffcx: ["https://docs.fenicsproject.org/ffcx/main/", null]
47+
ufl: ["https://docs.fenicsproject.org/ufl/main/", null]
48+
dolfinx: ["https://docs.fenicsproject.org/dolfinx/main/python", null]
49+
petsc4py: ["https://petsc.org/release/petsc4py", null]
50+
mpi4py: ["https://mpi4py.readthedocs.io/en/stable", null]
51+
numpy: ["https://numpy.org/doc/stable/", null]
52+
pyvista: ["https://docs.pyvista.org/", null]
53+
packaging: ["https://packaging.pypa.io/en/stable/", null]
54+
matplotlib: ["https://matplotlib.org/stable/", null]
5455

5556
extra_extensions:
56-
- 'sphinx.ext.autodoc'
57-
- 'sphinx.ext.intersphinx'
57+
- "sphinx.ext.autodoc"
58+
- "sphinx.ext.intersphinx"
5859
- "sphinx_codeautolink"
5960

6061
parse:

chapter1/membrane_code.ipynb

Lines changed: 83 additions & 45 deletions
Large diffs are not rendered by default.

chapter1/membrane_code.py

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,58 +19,70 @@
1919
# In this section, we will solve the deflection of the membrane problem.
2020
# After finishing this section, you should be able to:
2121
# - Create a simple mesh using the GMSH Python API and load it into DOLFINx
22-
# - Create constant boundary conditions using a geometrical identifier
23-
# - Use `ufl.SpatialCoordinate` to create a spatially varying function
24-
# - Interpolate a `ufl.Expression` into an appropriate function space
25-
# - Evaluate a `dolfinx.fem.Function` at any point $x$
22+
# - Create constant boundary conditions using a {py:func}`geometrical identifier<dolfinx.fem.locate_dofs_geometrical>`
23+
# - Use {py:class}`ufl.SpatialCoordinate` to create a spatially varying function
24+
# - Interpolate a {py:class}`ufl-Expression<ufl.core.expr.Expr>` into an appropriate function space
25+
# - Evaluate a {py:class}`dolfinx.fem.Function` at any point $x$
2626
# - Use Paraview to visualize the solution of a PDE
2727
#
2828
# ## Creating the mesh
2929
#
30-
# 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.
30+
# To create the computational geometry, we use the Python-API of [GMSH](https://gmsh.info/).
31+
# We start by importing the gmsh-module and initializing it.
3132

3233
# +
3334
import gmsh
3435

3536
gmsh.initialize()
3637
# -
3738

38-
# 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.
39+
# The next step is to create the membrane and start the computations by the GMSH CAD kernel,
40+
# to generate the relevant underlying data structures.
41+
# The first arguments of `addDisk` are the x, y and z coordinate of the center of the circle,
42+
# while the two last arguments are the x-radius and y-radius.
3943

4044
membrane = gmsh.model.occ.addDisk(0, 0, 0, 1, 1)
4145
gmsh.model.occ.synchronize()
4246

43-
# 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.
47+
# After that, we make the membrane a physical surface, such that it is recognized by `gmsh` when generating the mesh.
48+
# As a surface is a two-dimensional entity, we add `2` as the first argument,
49+
# the entity tag of the membrane as the second argument, and the physical tag as the last argument.
50+
# In a later demo, we will get into when this tag matters.
4451

4552
gdim = 2
4653
gmsh.model.addPhysicalGroup(gdim, [membrane], 1)
4754

48-
# Finally, we generate the two-dimensional mesh. We set a uniform mesh size by modifying the GMSH options.
55+
# Finally, we generate the two-dimensional mesh.
56+
# We set a uniform mesh size by modifying the GMSH options.
4957

5058
gmsh.option.setNumber("Mesh.CharacteristicLengthMin", 0.05)
5159
gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.05)
5260
gmsh.model.mesh.generate(gdim)
5361

5462
# # Interfacing with GMSH in DOLFINx
55-
# We will import the GMSH-mesh directly from GMSH into DOLFINx via the `dolfinx.io.gmsh` interface.
63+
# We will import the GMSH-mesh directly from GMSH into DOLFINx via the {py:mod}`dolfinx.io.gmsh` interface.
5664
# The {py:mod}`dolfinx.io.gmsh` module contains two functions
5765
# 1. {py:func}`model_to_mesh<dolfinx.io.gmsh.model_to_mesh>` which takes in a `gmsh.model`
5866
# and returns a {py:class}`dolfinx.io.gmsh.MeshData` object.
5967
# 2. {py:func}`read_from_msh<dolfinx.io.gmsh.read_from_msh>` which takes in a path to a `.msh`-file
6068
# and returns a {py:class}`dolfinx.io.gmsh.MeshData` object.
6169
#
62-
# The `MeshData` object will contain a `dolfinx.mesh.Mesh`, under the attribute `mesh`.
70+
# The {py:class}`MeshData` object will contain a {py:class}`dolfinx.mesh.Mesh`,
71+
# under the attribute {py:attr}`mesh<dolfinx.io.gmsh.MeshData.mesh>`.
6372
# This mesh will contain all GMSH Physical Groups of the highest topological dimension.
6473
# ```{note}
6574
# If you do not use `gmsh.model.addPhysicalGroup` when creating the mesh with GMSH, it can not be read into DOLFINx.
6675
# ```
67-
# The `MeshData` object can also contain tags for all other `PhysicalGroups` that has been added to the mesh,
68-
# that being `vertex_tags`, `edge_tags`, `facet_tags` and `cell_tags`.
76+
# The {py:class}`MeshData<dolfinx.io.gmsh.MeshData>` object can also contain tags for
77+
# all other `PhysicalGroups` that has been added to the mesh, that being
78+
# {py:attr}`cell_tags<dolfinx.io.gmsh.MeshData.cell_tags>`, {py:attr}`facet_tags<dolfinx.io.gmsh.MeshData.facet_tags>`,
79+
# {py:attr}`ridge_tags<dolfinx.io.gmsh.MeshData.ridge_tags>` and
80+
# {py:attr}`peak_tags<dolfinx.io.gmsh.MeshData.peak_tags>`.
6981
# To read either `gmsh.model` or a `.msh`-file, one has to distribute the mesh to all processes used by DOLFINx.
7082
# As GMSH does not support mesh creation with MPI, we currently have a `gmsh.model.mesh` on each process.
7183
# To distribute the mesh, we have to specify which process the mesh was created on,
7284
# and which communicator rank should distribute the mesh.
73-
# The `model_to_mesh` will then load the mesh on the specified rank,
85+
# The {py:func}`model_to_mesh<dolfinx.io.gmsh.model_to_mesh>` will then load the mesh on the specified rank,
7486
# and distribute it to the communicator using a mesh partitioner.
7587

7688
# +
@@ -95,18 +107,25 @@
95107
# -
96108

97109
# ## Defining a spatially varying load
98-
# The right hand side pressure function is represented using `ufl.SpatialCoordinate` and two constants, one for $\beta$ and one for $R_0$.
110+
# The right hand side pressure function is represented using {py:class}`ufl.SpatialCoordinate` and two constants,
111+
# one for $\beta$ and one for $R_0$.
99112

113+
# +
100114
import ufl
101115
from dolfinx import default_scalar_type
102116

103117
x = ufl.SpatialCoordinate(domain)
104118
beta = fem.Constant(domain, default_scalar_type(12))
105119
R0 = fem.Constant(domain, default_scalar_type(0.3))
106120
p = 4 * ufl.exp(-(beta**2) * (x[0] ** 2 + (x[1] - R0) ** 2))
121+
# -
107122

108123
# ## Create a Dirichlet boundary condition using geometrical conditions
109-
# The next step is to create the homogeneous boundary condition. As opposed to the [first tutorial](./fundamentals_code.ipynb) we will use `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$.
124+
# The next step is to create the homogeneous boundary condition.
125+
# As opposed to the [first tutorial](./fundamentals_code.ipynb) we will use
126+
# {py:func}`locate_dofs_geometrical<dolfinx.fem.locate_dofs_geometrical>` to locate the degrees of freedom on the boundary.
127+
# As we know that our domain is a circle with radius 1, we know that any degree of freedom should be
128+
# located at a coordinate $(x,y)$ such that $\sqrt{x^2+y^2}=1$.
110129

111130
# +
112131
import numpy as np
@@ -119,7 +138,9 @@ def on_boundary(x):
119138
boundary_dofs = fem.locate_dofs_geometrical(V, on_boundary)
120139
# -
121140

122-
# As our Dirichlet condition is homogeneous (`u=0` on the whole boundary), we can initialize the `dolfinx.fem.dirichletbc` with a constant value, the degrees of freedom and the function space to apply the boundary condition on.
141+
# As our Dirichlet condition is homogeneous (`u=0` on the whole boundary), we can initialize the
142+
# {py:class}`dolfinx.fem.DirichletBC` with a constant value, the degrees of freedom and the function
143+
# space to apply the boundary condition on. We use the constructor {py:func}`dolfinx.fem.dirichletbc`.
123144

124145
bc = fem.dirichletbc(default_scalar_type(0), boundary_dofs, V)
125146

@@ -139,8 +160,13 @@ def on_boundary(x):
139160
)
140161
uh = problem.solve()
141162

142-
# ## Interpolation of a `ufl`-expression
143-
# 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 `dolfinx.Expression`. The expression takes in any `ufl`-expression, and a set of points on the reference element. We will use the interpolation points of the space we want to interpolate in to.
163+
# ## Interpolation of a UFL-expression
164+
# As we previously defined the load `p` as a spatially varying function,
165+
# we would like to interpolate this function into an appropriate function space for visualization.
166+
# To do this we use the class {py:class}`Expression<dolfinx.fem.Expression>`.
167+
# The expression takes in any UFL-expression, and a set of points on the reference element.
168+
# We will use the {py:attr}`interpolation points<dolfinx.fem.FiniteElement.interpolation_points>`
169+
# of the space we want to interpolate in to.
144170
# We choose a high order function space to represent the function `p`, as it is rapidly varying in space.
145171

146172
Q = fem.functionspace(domain, ("Lagrange", 5))
@@ -154,8 +180,7 @@ def on_boundary(x):
154180
from dolfinx.plot import vtk_mesh
155181
import pyvista
156182

157-
158-
# Extract topology from mesh and create pyvista mesh
183+
# Extract topology from mesh and create {py:class}`pyvista.UnstructuredGrid`
159184

160185
topology, cell_types, x = vtk_mesh(V)
161186
grid = pyvista.UnstructuredGrid(topology, cell_types, x)
@@ -190,7 +215,8 @@ def on_boundary(x):
190215

191216
# ## Making curve plots throughout the domain
192217
# Another way to compare the deflection and the load is to make a plot along the line $x=0$.
193-
# 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.
218+
# This is just a matter of defining a set of points along the $y$-axis and evaluating the
219+
# finite element functions $u$ and $p$ at these points.
194220

195221
tol = 0.001 # Avoid hitting the outside of the domain
196222
y = np.linspace(-1 + tol, 1 - tol, 101)
@@ -199,22 +225,40 @@ def on_boundary(x):
199225
u_values = []
200226
p_values = []
201227

202-
# 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$.
203-
# 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.
204-
# 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.
228+
# As a finite element function is the linear combination of all degrees of freedom,
229+
# $u_h(x)=\sum_{i=1}^N c_i \phi_i(x)$ where $c_i$ are the coefficients of $u_h$ and $\phi_i$
230+
# is the $i$-th basis function, we can compute the exact solution at any point in $\Omega$.
231+
# However, as a mesh consists of a large set of degrees of freedom (i.e. $N$ is large),
232+
# we want to reduce the number of evaluations of the basis function $\phi_i(x)$.
233+
# We do this by identifying which cell of the mesh $x$ is in.
234+
# This is efficiently done by creating a {py:class}`bounding box tree<dolfinx.geometry.BoundingBoxTree`
235+
# of the cells of the mesh,
236+
# allowing a quick recursive search through the mesh entities.
205237

206238
# +
207239
from dolfinx import geometry
208240

209241
bb_tree = geometry.bb_tree(domain, domain.topology.dim)
210242
# -
211243

212-
# 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)`.
213-
# 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
214-
# using `dolfinx.geometry.select_colliding_cells`, which measures the exact distance between the point and the cell (approximated as a convex hull for higher order geometries).
215-
# 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.
244+
# Now we can compute which cells the bounding box tree collides with using
245+
# {py:func}`dolfinx.geometry.compute_collisions_points`.
246+
# This function returns a list of cells whose bounding box collide for each input point.
247+
# As different points might have different number of cells, the data is stored in
248+
# {py:class}`dolfinx.graph.AdjacencyList`, where one can access the cells for the
249+
# `i`th point by calling `links(i)<dolfinx.graph.AdjacencyList.links>`.
250+
# However, as the bounding box of a cell spans more of $\mathbb{R}^n$ than the actual cell,
251+
# we check that the actual cell collides with the input point using
252+
# {py:func}`dolfinx.geometry.compute_colliding_cells`,
253+
# which measures the exact distance between the point and the cell
254+
# (approximated as a convex hull for higher order geometries).
255+
# This function also returns an adjacency-list, as the point might align with a facet,
256+
# edge or vertex that is shared between multiple cells in the mesh.
216257
#
217-
# 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.
258+
# Finally, we would like the code below to run in parallel,
259+
# when the mesh is distributed over multiple processors.
260+
# In that case, it is not guaranteed that every point in `points` is on each processor.
261+
# Therefore we create a subset `points_on_proc` only containing the points found on the current processor.
218262

219263
cells = []
220264
points_on_proc = []
@@ -227,14 +271,16 @@ def on_boundary(x):
227271
points_on_proc.append(point)
228272
cells.append(colliding_cells.links(i)[0])
229273

230-
# We now have a list of points on the processor, on in which cell each point belongs. We can then call `uh.eval` and `pressure.eval` to obtain the set of values for all the points.
231-
#
274+
# We now have a list of points on the processor, on in which cell each point belongs.
275+
# We can then call {py:meth}`uh.eval<dolfinx.fem.Function.eval>` and
276+
# {py:meth}`pressure.eval<dolfinx.fem.Function.eval>` to obtain the set of values for all the points.
232277

233278
points_on_proc = np.array(points_on_proc, dtype=np.float64)
234279
u_values = uh.eval(points_on_proc, cells)
235280
p_values = pressure.eval(points_on_proc, cells)
236281

237-
# As we now have an array of coordinates and two arrays of function values, we can use `matplotlib` to plot them
282+
# As we now have an array of coordinates and two arrays of function values,
283+
# we can use {py:mod}`matplotlib<matplotlib.pyplot>` to plot them
238284

239285
# +
240286
import matplotlib.pyplot as plt
@@ -253,7 +299,7 @@ def on_boundary(x):
253299
plt.legend()
254300
# -
255301

256-
# If executed in parallel as a python file, we save a plot per processor
302+
# If executed in parallel as a Python file, we save a plot per processor
257303

258304
plt.savefig(f"membrane_rank{MPI.COMM_WORLD.rank:d}.png")
259305

chapter2/amr.ipynb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
},
139139
"outputs": [],
140140
"source": [
141-
"\n",
142141
"grid = pyvista.UnstructuredGrid(*dolfinx.plot.vtk_mesh(mesh))\n",
143142
"grid.cell_data[\"ct\"] = ct.values\n",
144143
"\n",

chapter2/amr.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
# We use pyvista to visualize the mesh.
7373

7474
# + tags=["hide-input"]
75-
7675
grid = pyvista.UnstructuredGrid(*dolfinx.plot.vtk_mesh(mesh))
7776
grid.cell_data["ct"] = ct.values
7877

chapter2/helmholtz_code.ipynb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
" assert mesh_data.facet_tags is not None\n",
9696
" facet_tags = mesh_data.facet_tags\n",
9797
"else:\n",
98-
" domain, _, facet_tags = mesh_data\n"
98+
" domain, _, facet_tags = mesh_data"
9999
]
100100
},
101101
{
@@ -113,15 +113,14 @@
113113
"metadata": {},
114114
"outputs": [],
115115
"source": [
116-
"\n",
117116
"V = fem.functionspace(domain, (\"Lagrange\", 1))\n",
118117
"\n",
119118
"# Discrete frequency range\n",
120119
"freq = np.arange(10, 1000, 5) # Hz\n",
121120
"\n",
122121
"# Air parameters\n",
123122
"rho0 = 1.225 # kg/m^3\n",
124-
"c = 340 # m/s\n"
123+
"c = 340 # m/s"
125124
]
126125
},
127126
{

chapter2/helmholtz_code.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,11 @@
8989
facet_tags = mesh_data.facet_tags
9090
else:
9191
domain, _, facet_tags = mesh_data
92-
9392
# -
9493

9594
# We define the function space for our unknown $p$ and define the range of frequencies we want to solve the Helmholtz equation for.
9695

9796
# +
98-
9997
V = fem.functionspace(domain, ("Lagrange", 1))
10098

10199
# Discrete frequency range
@@ -104,7 +102,6 @@
104102
# Air parameters
105103
rho0 = 1.225 # kg/m^3
106104
c = 340 # m/s
107-
108105
# -
109106

110107
# ## Boundary conditions

chapter2/nonlinpoisson_code.ipynb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@
2828
"import numpy\n",
2929
"\n",
3030
"from mpi4py import MPI\n",
31-
"from petsc4py import PETSc\n",
3231
"\n",
33-
"from dolfinx import mesh, fem, log\n",
32+
"from dolfinx import mesh, fem\n",
3433
"from dolfinx.fem.petsc import NonlinearProblem\n",
3534
"\n",
3635
"\n",

chapter2/nonlinpoisson_code.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131
import numpy
3232

3333
from mpi4py import MPI
34-
from petsc4py import PETSc
3534

36-
from dolfinx import mesh, fem, log
35+
from dolfinx import mesh, fem
3736
from dolfinx.fem.petsc import NonlinearProblem
3837

3938

chapter2/ns_code1.ipynb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,9 @@
501501
"cell_type": "code",
502502
"execution_count": null,
503503
"id": "26",
504-
"metadata": {},
504+
"metadata": {
505+
"lines_to_end_of_cell_marker": 2
506+
},
505507
"outputs": [],
506508
"source": [
507509
"from pathlib import Path\n",
@@ -516,7 +518,9 @@
516518
},
517519
{
518520
"cell_type": "markdown",
519-
"metadata": {},
521+
"metadata": {
522+
"lines_to_next_cell": 2
523+
},
520524
"source": [
521525
"We also interpolate the analytical solution into our function-space and create a variational formulation for the $L^2$-error.\n"
522526
]
@@ -527,8 +531,6 @@
527531
"metadata": {},
528532
"outputs": [],
529533
"source": [
530-
"\n",
531-
"\n",
532534
"def u_exact(x):\n",
533535
" values = np.zeros((2, x.shape[1]), dtype=PETSc.ScalarType)\n",
534536
" values[0] = 4 * x[1] * (1.0 - x[1])\n",

0 commit comments

Comments
 (0)