Skip to content

Commit d083087

Browse files
committed
links
1 parent b88fb70 commit d083087

3 files changed

Lines changed: 119 additions & 91 deletions

File tree

-33.7 KB
Binary file not shown.
Lines changed: 108 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,59 @@
1-
Adaptive Multigrid Methods using the AdaptiveMeshHierarchy
2-
=================================================
1+
Adaptive Multigrid Methods using AdaptiveMeshHierarchy
2+
======================================================
3+
4+
5+
Contributed by Anurag Rao
36

47
The purpose of this demo is to show how to use Firedrake's multigrid solver on a hierarchy of adaptively refined Netgen meshes.
5-
We will first have a look at how to use the *AdaptiveMeshHierarchy* to construct the mesh hierarchy with Netgen meshes, then we will consider a solution to the Poisson problem on an L-shaped domain.
6-
Finally, we will show how to use the *AdaptiveMeshHierarchy* and *AdaptiveTransferManager* as arguments to Firedrake solvers. The *AdaptiveMeshHierarchy* contains information of the mesh hierarchy and the parent child relations between the meshes.
7-
The *AdaptiveTransferManager* deals with the transfer operator logic across any given levels in the hierarchy.
8+
We will first have a look at how to use the :class:`.AdaptiveMeshHierarchy` to construct the mesh hierarchy with Netgen meshes, then we will consider a solution to the Poisson problem on an L-shaped domain.
9+
Finally, we will show how to use the :class:`.AdaptiveMeshHierarchy` and :class:`.AdaptiveTransferManager` to construct a scalable solver. The :class:`.AdaptiveMeshHierarchy` contains information of the mesh hierarchy and the parent child relations between the meshes.
10+
The :class:`.AdaptiveTransferManager` deals with the transfer operator logic across any given levels in the hierarchy.
811
We begin by importing the necessary libraries ::
912

1013
from firedrake import *
1114
from netgen.occ import *
12-
from ngsPETSc import AdaptiveMeshHierarchy, AdaptiveTransferManager
15+
import numpy
1316

1417
Constructing the Mesh Hierarchy
15-
---------------------------
16-
We first must construct the domain over which we will solve the problem. For a more comprehensive demo on how to use Open Cascade Technology (OCC) and Constructive Solid Geometry (CSG), see `Netgen integration in Firedrake <https://www.firedrakeproject.org/demos/netgen_mesh.py.html>`_ .
18+
-------------------------------
19+
We first must construct the domain over which we will solve the problem. For a more comprehensive demo on how to use Open Cascade Technology (OCC) and Constructive Solid Geometry (CSG),
20+
see `Netgen integration in Firedrake <netgen_mesh.py>`_.
1721
We begin with the L-shaped domain, which we build as the union of two rectangles: ::
18-
rect1 = WorkPlane(Axes((0,0,0), n=Z, h=X)).Rectangle(1,2).Face()
19-
rect2 = WorkPlane(Axes((0,1,0), n=Z, h=X)).Rectangle(2,1).Face()
20-
L = rect1 + rect2
2122
22-
geo = OCCGeometry(L, dim=2)
23-
ngmsh = geo.GenerateMesh(maxh=0.5)
24-
mesh = Mesh(ngmsh)
23+
rect1 = WorkPlane(Axes((0,0,0), n=Z, h=X)).Rectangle(1,2).Face()
24+
rect2 = WorkPlane(Axes((0,1,0), n=Z, h=X)).Rectangle(2,1).Face()
25+
L = rect1 + rect2
26+
27+
geo = OCCGeometry(L, dim=2)
28+
ngmsh = geo.GenerateMesh(maxh=0.5)
29+
mesh = Mesh(ngmsh)
2530

26-
It is important to convert the initial Netgen mesh into a Firedrake mesh before constructing the *AdaptiveMeshHierarchy*. To call the constructor to the hierarchy, we must pass the initial mesh. Our initial mesh looks like this:
31+
It is important to convert the initial Netgen mesh into a Firedrake mesh before constructing the :class:`.AdaptiveMeshHierarchy`. To call the constructor to the hierarchy, we must pass the initial mesh. Our initial mesh looks like this:
2732

2833
.. figure:: initial_mesh.png
2934
:align: center
3035
:alt: Initial mesh.
3136

32-
We will also initialize the *AdaptiveTransferManager* here: ::
37+
We will also initialize the :class:`.AdaptiveTransferManager` here: ::
3338
34-
amh = AdaptiveMeshHierarchy(mesh)
35-
atm = AdaptiveTransferManager()
39+
amh = AdaptiveMeshHierarchy(mesh)
40+
atm = AdaptiveTransferManager()
3641

3742
Poisson Problem
38-
-------------------------
43+
---------------
3944
Now we can define a simple Poisson problem
4045

4146
.. math::
4247
4348
- \nabla^2 u = f \text{ in } \Omega, \quad u = 0 \text{ on } \partial \Omega
4449
45-
Our approach strongly follows the similar problem in this `lecture course <https://github.com/pefarrell/icerm2024>`_. We define the function ``solve_poisson``. The first lines correspond to finding a solution in the CG1 space. The variational problem is formulated with F, where f is the constant function equal to 1. Since we want Dirichlet boundary conditions, we construct the *DirichletBC* object and apply it to the entire boundary: ::
50+
Our approach strongly follows the similar problem in this `lecture course <https://github.com/pefarrell/icerm2024>`_. We define the function ``solve_poisson``. The first lines correspond to finding a solution in the CG1 space. The variational problem is formulated with F, where f is the constant function equal to 1. Since we want Dirichlet boundary conditions, we construct the :class:`.DirichletBC` object and apply it to the entire boundary: ::
4651

4752
def solve_poisson(mesh, params):
4853
V = FunctionSpace(mesh, "CG", 1)
49-
uh = Function(V, name="Solution")
54+
uh = Function(V, name="solution")
5055
v = TestFunction(V)
51-
bc = DirichletBC(V, Constant(0), "on_boundary")
56+
bc = DirichletBC(V, 0, "on_boundary")
5257
f = Constant(1)
5358
F = inner(grad(uh), grad(v))*dx - inner(f, v)*dx
5459

@@ -58,57 +63,47 @@ Our approach strongly follows the similar problem in this `lecture course <https
5863
solver.solve()
5964
return uh
6065

61-
Note the code after the construction of the *NonlinearVariationalProblem()*. To use the *AdaptiveMeshHierarchy* with the existing Firedrake solver, we have to set the *AdaptiveTransferManager* as the transfer manager of the multigrid solver.
62-
For the parameters of the multigrid solver, we will be using patch relaxation, which we define with ::
66+
Note the code after the construction of the :class:`.NonlinearVariationalProblem`. To use the :class:`.AdaptiveMeshHierarchy` with the existing Firedrake solver, we have to set the :class:`.AdaptiveTransferManager` as the transfer manager of the multigrid solver.
67+
Since we are using linear CG elements, we will employ Jacobi as the multigrid relaxation, which we define with ::
68+
6369
solver_params = {
64-
"mat_type": "matfree",
65-
"ksp_type": "cg",
66-
"pc_type": "mg",
67-
"mg_levels": {
68-
"ksp_type": "chebyshev",
69-
"ksp_max_it": 1,
70-
"pc_type": "python",
71-
"pc_python_type": "firedrake.PatchPC",
72-
"patch": {
73-
"pc_patch": {
74-
"construct_type": "star",
75-
"construct_dim": 0,
76-
"sub_mat_type": "seqdense",
77-
"dense_inverse": True,
78-
"save_operators": True,
79-
"precompute_element_tensors": True,
80-
},
81-
"sub_ksp_type": "preonly",
82-
"sub_pc_type": "lu",
83-
},
84-
},
85-
"mg_coarse": {
86-
"ksp_type": "preonly",
87-
"pc_type": "python",
88-
"pc_python_type": "firedrake.AssembledPC",
89-
"assembled": {"ksp_type": "preonly", "pc_type": "lu"},
90-
},
91-
}
92-
93-
For more information about patch relaxation, see `Using patch relaxation for multigrid <https://www.firedrakeproject.org/demos/poisson_mg_patches.py.html>`_. The initial solution is shown below.
70+
"mat_type": "matfree",
71+
"ksp_type": "cg",
72+
"pc_type": "mg",
73+
"mg_levels": {
74+
"ksp_type": "chebyshev",
75+
"ksp_max_it": 1,
76+
"pc_type": "jacobi",
77+
},
78+
"mg_coarse": {
79+
"mat_type": "aij",
80+
"pc_type": "lu",
81+
},
82+
}
83+
84+
Alternatively for high-order CG elements, it is recommended to use patch relaxation
85+
to achieve degree-independent multigrid convergence.
86+
For more information
87+
see :doc:`Using patch relaxation for multigrid <poisson_mg_patches.py>`.
88+
The initial solution is shown below.
9489

9590
.. figure:: solution_l1.png
9691
:align: center
9792
:alt: Initial Solution from multigrid with initial mesh.
9893

9994

10095
Adaptive Mesh Refinement
101-
-------------------------
102-
In this section we will discuss how to adaptively refine select elements and add the newly refined mesh into the *AdaptiveMeshHierarchy*.
96+
------------------------
97+
In this section we will discuss how to adaptively refine select elements and add the newly refined mesh into the :class:`.AdaptiveMeshHierarchy`.
10398
For this problem, we will be using the Babuška-Rheinbolt a-posteriori estimate for an element:
10499

105100
.. math::
106-
\eta_K^2 = h_K^2 \int_K | f + \nabla^2 u_h |^2 \mathrm{d}x + \frac{h_K}{2} \int_{\partial K \setminus \partial \Omega} \llbracket \nabla u_h \cdot n \rrbracket^2 \mathrm{d}s,
101+
\eta_K^2 = h_K^2 \int_K \| f + \nabla^2 u_h \|^2 \mathrm{d}x + \frac{h_K}{2} \int_{\partial K \setminus \partial \Omega} \nabla u_h \cdot n ^2 \mathrm{d}s,
107102
108-
where :math:`K` is the element, :math:`h_K` is the diameter of the element, :math:`n` is the normal, and :math:`\llbracket \cdot \rrbracket` is the jump operator. The a-posteriori estimator is computed using the solution at the current level :math:`h`. Integrating over the domain and using the fact that the components of the estimator are piecewise constant on each cell, we can transform the above estimator into the variational problem
103+
where :math:`K` is the element, :math:`h_K` is the diameter of the element, :math:`n` is the normal, and :math:` \cdot ` is the jump operator. The a-posteriori estimator is computed using the solution at the current level :math:`h`. Integrating over the domain and using the fact that the components of the estimator are piecewise constant on each cell, we can transform the above estimator into the variational problem
109104

110105
.. math::
111-
\int_\Omega \eta_K^2 w \mathrm{d}x = \int_\Omega \sum_K h_K^2 \int_K (f + \text{div} (\text{grad} u_h) )^2 \mathrm{d}x w \mathrm{d}x + \int_\Omega \sum_K \frac{h_K}{2} \int_{\partial K \setminus \partial \Omega} \llbracket \nabla u_h \cdot n \rrbracket^2 \mathrm{d}s w \mathrm{d}x
106+
\int_\Omega \eta_K^2 w \mathrm{d}x = \int_\Omega \sum_K h_K^2 \int_K (f + \text{div} (\text{grad} u_h) )^2 \mathrm{d}x w \mathrm{d}x + \int_\Omega \sum_K \frac{h_K}{2} \int_{\partial K \setminus \partial \Omega} \nabla u_h \cdot n ^2 \mathrm{d}s w \mathrm{d}x
112107
113108
Our approach will be to compute the estimator over all elements and selectively choose to refine only those that contribute most to the error. To compute the error estimator, we use the function below to solve the variational formulation of the error estimator. Since our estimator is a constant per element, we use a DG0 function space. ::
114109

@@ -117,54 +112,73 @@ Our approach will be to compute the estimator over all elements and selectively
117112
eta_sq = Function(W)
118113
w = TestFunction(W)
119114
f = Constant(1)
120-
h = CellDiameter(mesh) # symbols for mesh quantities
115+
116+
# symbols for mesh quantities
117+
h = CellDiameter(mesh)
121118
n = FacetNormal(mesh)
122119
v = CellVolume(mesh)
123120
124-
G = ( # compute cellwise error estimator
125-
inner(eta_sq / v, w)*dx
126-
- inner(h**2 * (f + div(grad(uh)))**2, w) * dx
127-
- inner(h('+')/2 * jump(grad(uh), n)**2, w('+')) * dS
128-
- inner(h('-')/2 * jump(grad(uh), n)**2, w('-')) * dS
129-
)
121+
# compute cellwise error estimator
122+
G = (inner(eta_sq / v, w)*dx
123+
- inner(h**2 * (f + div(grad(uh)))**2, w) * dx
124+
- inner(h('+')/2 * jump(grad(uh), n)**2, w('+')) * dS
125+
- inner(h('-')/2 * jump(grad(uh), n)**2, w('-')) * dS
126+
)
130127
131128
sp = {"mat_type": "matfree", "ksp_type": "richardson", "pc_type": "jacobi"}
132129
solve(G == 0, eta_sq, solver_parameters=sp)
133130
eta = Function(W).interpolate(sqrt(eta_sq)) # compute eta from eta^2
134131
135132
with eta.dat.vec_ro as eta_: # compute estimate for error in energy norm
136-
error_est = sqrt(eta_.dot(eta_))
137-
return (eta, error_est)
133+
error_est = eta_.norm()
134+
return eta, error_est
138135

139-
The next step is to choose which elements to refine. For this we use Dörfler marking [Dörfler1996]:
136+
The next step is to choose which elements to refine. For this we use Dörfler marking :cite:`Dorfler1996`:
140137

141138
.. math::
142139
\eta_K \geq \theta \text{max}_L \eta_L
143140
144-
The logic is to select an element :math:`K` to refine if the estimator is greater than some factor :math:`\theta` of the maximum error estimate of the mesh, where :math:`\theta` ranges from 0 to 1. In our code we choose :math:`theta=0.5`.
141+
The logic is to select an element :math:`K` to refine if the estimator is greater than some factor :math:`\theta` of the maximum error estimate of the mesh, where :math:`\theta` ranges from 0 to 1. In our code we choose :math:`\theta=0.5`.
145142
With these helper functions complete, we can solve the system iteratively. In the max_iterations is the number of total levels we want to perform multigrid on. We will solve for 15 levels. At every level :math:`l`, we first compute the solution using multigrid with patch relaxation up till level :math:`l`. We then use the current approximation of the solution to estimate the error across the mesh. Finally, we refine the mesh and repeat. ::
146143

144+
theta = 0.5
147145
max_iterations = 15
148-
error_estimators = []
146+
est_errors = []
149147
dofs = []
150148
for i in range(max_iterations):
151-
print(f"level {i}")
152-
153-
uh = solve_poisson(mesh, patch_relax)
154-
VTKFile(f"output/poisson_l/{max_iterations}/adaptive_loop_{i}.pvd").write(uh)
155-
156-
(eta, error_est) = estimate_error(mesh, uh)
157-
VTKFile(f"output/poisson_l/{max_iterations}/eta_{i}.pvd").write(eta)
158-
159-
print(f" ||u - u_h|| <= C x {error_est}")
160-
error_estimators.append(error_est)
161-
dofs.append(uh.function_space().dim())
162-
163-
if i != max_iterations - 1:
164-
amh.adapt(eta, theta)
149+
print(f"level {i}")
150+
151+
mesh = amh[-1]
152+
uh = solve_poisson(mesh, solver_params)
153+
VTKFile(f"output/adaptive_loop_{i}.pvd").write(uh)
165154

166-
To perform Dörfler marking, refine the current mesh, and add the mesh to the *AdaptiveMeshHierarchy*, we us the ``amh.adapt(eta, theta)`` method. In this method the input is the recently computed error estimator :math:`eta` and the Dörfler marking parameter :math:`theta`. The method always performs this on the current fine mesh in the hierarchy. There is another method for adding a mesh to the hierarchy: ``amh.add_mesh(mesh)``. In this method, refinement on the mesh is performed externally by some custom procedure and the resulting mesh directly gets added to the hierarchy.
167-
The meshes now refine according to the error estimator. The error estimators at levels 3,5, and 15 are shown below. Zooming into the vertex of the L at level 15 shows the error indicator remains strongest there. Further refinements will focus on that area.
155+
(eta, error_est) = estimate_error(mesh, uh)
156+
VTKFile(f"output/eta_{i}.pvd").write(eta)
157+
158+
est_errors.append(error_est)
159+
dofs.append(uh.function_space().dim())
160+
161+
print(f" ||u - u_h|| <= C * {error_est}")
162+
if len(dofs) > 1:
163+
rate = -numpy.diff(numpy.log(est_errors)) / numpy.diff(numpy.log(dofs))
164+
print(f" rate = {rate[-1]}")
165+
166+
if i != max_iterations - 1:
167+
amh.adapt(eta, theta)
168+
169+
from matplotlib import pyplot as plt
170+
171+
opt_errors = est_errors[0] * (numpy.array(dofs)/dofs[0]) ** -0.5
172+
plt.loglog(dofs, est_errors, '-o', markersize = 3, label="Estimated error")
173+
plt.loglog(dofs, opt_errors, '--', markersize = 3, label="Optimal convergence")
174+
plt.ylabel("Error estimate of the energy norm")
175+
plt.xlabel("Number of degrees of freedom")
176+
plt.legend()
177+
plt.savefig("output/adaptive_convergence.png")
178+
179+
180+
To perform Dörfler marking, refine the current mesh, and add the mesh to the :class:`.AdaptiveMeshHierarchy`, we use the ``amh.adapt(eta, theta)`` method. In this method the input is the recently computed error estimator ``eta`` and the Dörfler marking parameter ``theta``. The method always performs this on the current fine mesh in the hierarchy. There is another method for adding a mesh to the hierarchy: ``amh.add_mesh(mesh)``. In this method, refinement on the mesh is performed externally by some custom procedure and the resulting mesh directly gets added to the hierarchy.
181+
The meshes now refine according to the error estimator. The error estimators at levels 3,5, and 15 are shown below. Zooming into the vertex of the L-shape at level 15 shows the error indicator remains strongest there. Further refinements will focus on that area.
168182

169183
+-------------------------------+-------------------------------+-------------------------------+
170184
| .. figure:: eta_l3.png | .. figure:: eta_l6.png | .. figure:: eta_l15.png |
@@ -189,11 +203,14 @@ The solutions at level 4 and 15 are shown below.
189203

190204
The convergence follows the expected optimal behavior:
191205

192-
.. figure:: adaptive_convergence_9.png
206+
.. figure:: adaptive_convergence.png
193207
:align: center
194208
:alt: Convergence of the error estimator.
195209

196-
References
197-
-------------------------
198-
[Dörfler1996] W. Dörfler. A convergent adaptive algorithm for poisson’s equation. SIAM Journal on Numerical Analysis, 33(3):1106–1124, 1996.
199210

211+
A runnable python version of this demo can be found :demo:`here<adaptive_multigrid.py>`.
212+
213+
.. rubric:: References
214+
215+
.. bibliography:: demo_references.bib
216+
:filter: docname in docnames

demos/demo_references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,14 @@ @article{Brubeck2024
357357
volume = {46},
358358
year = {2024}
359359
}
360+
361+
@article{Dorfler1996,
362+
title={A convergent adaptive algorithm for Poisson’s equation},
363+
author={D{\"o}rfler, Willy},
364+
journal={SIAM Journal on Numerical Analysis},
365+
volume={33},
366+
number={3},
367+
pages={1106--1124},
368+
year={1996},
369+
publisher={SIAM}
370+
}

0 commit comments

Comments
 (0)