Skip to content

Commit 97bf448

Browse files
pbrubeckAnuragRao1pefarrell
authored andcommitted
Adaptive Multigrid (#4726)
* Adaptive Multigrid * Added demo for performing adaptive multigrid (#4535) --------- Co-authored-by: AnuragRao1 <102687968+AnuragRao1@users.noreply.github.com> Co-authored-by: Patrick Farrell <patrick.farrell@maths.ox.ac.uk>
1 parent 8b51d35 commit 97bf448

17 files changed

Lines changed: 781 additions & 1 deletion
33.3 KB
Loading
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
Adaptive Multigrid Methods using AdaptiveMeshHierarchy
2+
======================================================
3+
4+
5+
Contributed by Anurag Rao.
6+
7+
The purpose of this demo is to show how to use Firedrake's multigrid solver on a hierarchy of adaptively refined Netgen meshes.
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.
11+
We begin by importing the necessary libraries ::
12+
13+
from firedrake import *
14+
from netgen.occ import *
15+
import numpy
16+
17+
Constructing the Mesh Hierarchy
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>`_.
21+
We begin with the L-shaped domain, which we build as the union of two rectangles: ::
22+
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)
30+
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:
32+
33+
.. figure:: initial_mesh.png
34+
:align: center
35+
:alt: Initial mesh.
36+
37+
We will also initialize the :class:`~.AdaptiveTransferManager` here: ::
38+
39+
amh = AdaptiveMeshHierarchy(mesh)
40+
atm = AdaptiveTransferManager()
41+
42+
Poisson Problem
43+
---------------
44+
Now we can define a simple Poisson problem
45+
46+
.. math::
47+
48+
- \nabla^2 u = f \text{ in } \Omega, \quad u = 0 \text{ on } \partial \Omega.
49+
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 right-hand side is set to be 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: ::
51+
52+
def solve_poisson(mesh, params):
53+
V = FunctionSpace(mesh, "CG", 1)
54+
v = TestFunction(V)
55+
u = TrialFunction(V)
56+
uh = Function(V, name="solution")
57+
bcs = [DirichletBC(V, 0, "on_boundary")]
58+
f = Constant(1)
59+
60+
a = inner(grad(u), grad(v))*dx
61+
L = inner(f, v)*dx
62+
63+
problem = LinearVariationalProblem(a, L, uh, bcs)
64+
solver = LinearVariationalSolver(problem, solver_parameters=params)
65+
66+
solver.set_transfer_manager(atm)
67+
solver.solve()
68+
69+
its = solver.snes.getLinearSolveIterations()
70+
return uh, its
71+
72+
Note the code after the construction of the :class:`~.LinearVariationalProblem`. 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.
73+
Since we are using linear Lagrange elements, we will employ Jacobi as the multigrid relaxation, which we define with ::
74+
75+
solver_params = {
76+
"mat_type": "matfree",
77+
"ksp_type": "cg",
78+
"pc_type": "mg",
79+
"mg_levels": {
80+
"ksp_type": "chebyshev",
81+
"ksp_max_it": 1,
82+
"pc_type": "jacobi",
83+
},
84+
"mg_coarse": {
85+
"mat_type": "aij",
86+
"pc_type": "lu",
87+
},
88+
}
89+
90+
Alternatively for high-order CG elements, it is recommended to use patch relaxation
91+
to achieve degree-independent multigrid convergence.
92+
For more information
93+
see :doc:`Using patch relaxation for multigrid <poisson_mg_patches.py>`.
94+
The initial solution is shown below.
95+
96+
.. figure:: solution_l1.png
97+
:align: center
98+
:alt: Initial Solution from multigrid with initial mesh.
99+
100+
101+
Adaptive Mesh Refinement
102+
------------------------
103+
In this section we will discuss how to adaptively refine select elements and add the newly refined mesh into the :class:`~.AdaptiveMeshHierarchy`.
104+
For this problem, we will be using the Babuška-Rheinbolt a-posteriori estimate for an element:
105+
106+
.. math::
107+
\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} \left[[ \nabla u_h \cdot \mathbf{n} \right]]^2 \mathrm{d}s,
108+
109+
where :math:`K` is the element, :math:`h_K` is the diameter of the element, :math:`\mathbf{n}` is the outward-facing normal, and :math:`\left[[ \cdot \right]]` 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
110+
111+
.. math::
112+
\int_\Omega \eta_K^2 q \,\mathrm{d}x = \int_\Omega \sum_K h_K^2 \int_K (f + \text{div} (\text{grad} u_h) )^2 \,\mathrm{d}x q \,\mathrm{d}x + \int_\Omega \sum_K \frac{h_K}{2} \int_{\partial K \setminus \partial \Omega} \left[[ \nabla u_h \cdot \mathbf{n} \right]]^2 \,\mathrm{d}s q \,\mathrm{d}x \quad \forall\, q \in \mathrm{DG}_0
113+
114+
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. ::
115+
116+
def estimate_error(mesh, uh):
117+
Q = FunctionSpace(mesh, "DG", 0)
118+
eta_sq = Function(Q)
119+
p = TrialFunction(Q)
120+
q = TestFunction(Q)
121+
f = Constant(1)
122+
residual = f + div(grad(uh))
123+
124+
# symbols for mesh quantities
125+
h = CellDiameter(mesh)
126+
n = FacetNormal(mesh)
127+
vol = CellVolume(mesh)
128+
129+
# compute cellwise error estimator
130+
a = inner(p, q / vol) * dx
131+
L = (inner(residual**2, q * h**2) * dx
132+
+ inner(jump(grad(uh), n)**2, avg(q * h)) * dS
133+
)
134+
135+
sp = {"mat_type": "matfree", "ksp_type": "preonly", "pc_type": "jacobi"}
136+
solve(a == L, eta_sq, solver_parameters=sp)
137+
138+
# compute eta from eta^2
139+
eta = Function(Q).interpolate(sqrt(eta_sq))
140+
141+
# compute estimate for error in energy norm
142+
with eta.dat.vec_ro as eta_:
143+
error_est = eta_.norm()
144+
return eta, error_est
145+
146+
The next step is to choose which elements to refine. For this we use a simplified variant of Dörfler marking :cite:`Dorfler1996`:
147+
148+
.. math::
149+
\eta_K \geq \theta \text{max}_L \eta_L
150+
151+
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`.
152+
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 up to level :math:`l`. We then use the current approximation of the solution to estimate the error across the mesh. Finally, we adaptively refine the mesh and repeat. ::
153+
154+
theta = 0.5
155+
refinements = 15
156+
est_errors = []
157+
sqrt_dofs = []
158+
mg_iterations = []
159+
for level in range(refinements):
160+
print(f"level {level}")
161+
162+
mesh = amh[-1]
163+
uh, its = solve_poisson(mesh, solver_params)
164+
VTKFile(f"output/adaptive_loop_{level}.pvd").write(uh)
165+
166+
(eta, error_est) = estimate_error(mesh, uh)
167+
VTKFile(f"output/eta_{level}.pvd").write(eta)
168+
169+
est_errors.append(error_est)
170+
sqrt_dofs.append(uh.function_space().dim() ** 0.5)
171+
mg_iterations.append(its)
172+
173+
print(f" ||u - u_h|| <= C * {error_est}")
174+
if len(est_errors) > 1:
175+
rates = -numpy.diff(numpy.log(est_errors)) / numpy.diff(numpy.log(sqrt_dofs))
176+
print(f" rate = {rates[-1]}")
177+
178+
if i != refinements - 1:
179+
amh.adapt(eta, theta)
180+
181+
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.
182+
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.
183+
184+
+-------------------------------+-------------------------------+-------------------------------+
185+
| .. figure:: eta_l3.png | .. figure:: eta_l6.png | .. figure:: eta_l15.png |
186+
| :align: center | :align: center | :align: center |
187+
| :height: 250px | :height: 250px | :height: 250px |
188+
| :alt: Eta at level 3 | :alt: Eta at level 6 | :alt: Eta at level 15 |
189+
| | | |
190+
| *Level 3* | *Level 6* | *Level 15* |
191+
+-------------------------------+-------------------------------+-------------------------------+
192+
193+
The solutions at level 4 and 15 are shown below.
194+
195+
+------------------------------------+------------------------------------+
196+
| .. figure:: solution_l4.png | .. figure:: solution_l15.png |
197+
| :align: center | :align: center |
198+
| :height: 300px | :height: 300px |
199+
| :alt: Solution, level 4 | :alt: Solution, level 15 |
200+
| | |
201+
| *MG solution at level 4* | *MG solution at level 15* |
202+
+------------------------------------+------------------------------------+
203+
204+
The convergence follows the expected optimal behavior: ::
205+
206+
from matplotlib import pyplot as plt
207+
208+
dofs = numpy.array(sqrt_dofs) ** 2
209+
opt_errors = est_errors[0] * (sqrt_dofs[0] / numpy.array(sqrt_dofs))
210+
plt.loglog(dofs, est_errors, '-o', markersize = 3, label="Estimated error")
211+
plt.loglog(dofs, opt_errors, '--', markersize = 3, label="Optimal convergence")
212+
plt.ylabel("Error estimate of the energy norm")
213+
plt.xlabel("Number of degrees of freedom")
214+
plt.legend()
215+
plt.savefig("output/adaptive_convergence.png")
216+
217+
.. figure:: adaptive_convergence.png
218+
:align: center
219+
:alt: Convergence of the error estimator.
220+
221+
Moreover, the multigrid iteration count is robust to the level of refinement ::
222+
223+
print(" Level\t | Iterations")
224+
print("---------------------")
225+
for level, its in enumerate(mg_iterations):
226+
print(f" {level}\t | {its}")
227+
228+
..
229+
230+
======== ================
231+
Level Iterations
232+
======== ================
233+
0 2
234+
1 8
235+
2 8
236+
3 8
237+
4 8
238+
5 8
239+
6 8
240+
7 8
241+
8 8
242+
9 9
243+
10 9
244+
11 9
245+
12 9
246+
13 9
247+
14 9
248+
======== ================
249+
250+
A runnable python version of this demo can be found :demo:`here<adaptive_multigrid.py>`.
251+
252+
.. rubric:: References
253+
254+
.. bibliography:: demo_references.bib
255+
:filter: docname in docnames
331 KB
Loading
46 KB
Loading
84.3 KB
Loading
43.9 KB
Loading
31.6 KB
Loading
152 KB
Loading
102 KB
Loading

demos/demo_references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,17 @@ @article{Brubeck2024
370370
year = {2024}
371371
}
372372

373+
@article{Dorfler1996,
374+
title={A convergent adaptive algorithm for Poisson’s equation},
375+
author={D{\"o}rfler, Willy},
376+
journal={SIAM Journal on Numerical Analysis},
377+
volume={33},
378+
number={3},
379+
pages={1106--1124},
380+
year={1996},
381+
publisher={SIAM}
382+
}
383+
373384
@Article{Farrell2015,
374385
author = {Patrick E. Farrell and \'Asgeir Birkisson and Simon W. Funke},
375386
title = {{Deflation techniques for finding distinct solutions of nonlinear partial differential equations}},

0 commit comments

Comments
 (0)