Skip to content

Commit 9965289

Browse files
committed
DG examples + analysis
1 parent a0be232 commit 9965289

7 files changed

Lines changed: 734 additions & 109 deletions

File tree

pySDC/implementations/transfer_classes/TransferFenicsMesh.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,51 @@ def Pmat(self):
2121
"""
2222
Prolongation matrix P (coarse -> fine), assembled once on first use.
2323
24-
P is collected column by column from df.interpolate of the coarse basis functions.
25-
dolfin 2019.1.0's PETScDMCollection.create_transfer_matrix segfaults, and going through
26-
scipy has the side benefit that P^T is then free.
27-
28-
Costs one df.interpolate per coarse dof, so it is fine for moderate coarse spaces and
29-
wants the analytic construction (fine dof coords -> coarse cell -> coarse basis) if the
30-
coarse level ever gets large.
24+
P is the inclusion of the coarse space in the fine one: column j holds the coarse basis
25+
function phi_j expanded in the fine basis. It is built cell by cell -- for every fine cell
26+
the enclosing coarse cell is found from the fine cell's midpoint, which is never on a coarse
27+
facet, and the coarse basis is evaluated in *that* cell.
28+
29+
Going through df.interpolate instead, as this used to, is wrong for discontinuous spaces. A
30+
fine dof sitting on a coarse facet has two coarse values there, and dolfin's cross-mesh
31+
interpolate takes whichever cell the bounding-box tree returns first -- silently continuising
32+
the coarse function. The error is O(1) in the size of the jump and invisible for smooth data,
33+
but it deletes exactly the part of the coarse correction a DG hierarchy exists to carry. For
34+
continuous spaces the two constructions agree to machine precision.
35+
36+
Costs one basis evaluation per fine dof. dolfin 2019.1.0's
37+
PETScDMCollection.create_transfer_matrix segfaults, and going through scipy has the side
38+
benefit that P^T is then free.
3139
"""
3240
if self._Pmat is None:
3341
Vc, Vf = self.coarse_prob.init, self.fine_prob.init
34-
e_c, out_f = df.Function(Vc), df.Function(Vf)
42+
tree = Vc.mesh().bounding_box_tree()
43+
element, dofmap_c = Vc.element(), Vc.dofmap()
44+
x_f = Vf.tabulate_dof_coordinates().reshape(Vf.dim(), -1)
45+
46+
# which component of a mixed space each fine dof belongs to; scalar spaces are all zero
47+
ncomp = max(Vf.num_sub_spaces(), 1)
48+
component = np.zeros(Vf.dim(), dtype=int)
49+
for k in range(Vf.num_sub_spaces()):
50+
component[Vf.sub(k).dofmap().dofs()] = k
51+
3552
rows, cols, vals = [], [], []
36-
for j in range(Vc.dim()):
37-
e_c.vector().zero()
38-
e_c.vector()[j] = 1.0
39-
out_f.assign(df.interpolate(e_c, Vf))
40-
col = out_f.vector()[:]
41-
nz = np.nonzero(np.abs(col) > 1e-13)[0]
42-
rows.extend(nz)
43-
cols.extend([j] * len(nz))
44-
vals.extend(col[nz])
53+
seen = set()
54+
for cell_f in df.cells(Vf.mesh()):
55+
cell_c = df.Cell(Vc.mesh(), tree.compute_first_entity_collision(cell_f.midpoint()))
56+
coords, orientation = cell_c.get_vertex_coordinates(), cell_c.orientation()
57+
dofs_c = dofmap_c.cell_dofs(cell_c.index())
58+
for dof_f in Vf.dofmap().cell_dofs(cell_f.index()):
59+
# a continuous space shares dofs between cells; the second visit is redundant
60+
if dof_f in seen:
61+
continue
62+
seen.add(dof_f)
63+
basis = np.asarray(element.evaluate_basis_all(x_f[dof_f], coords, orientation))
64+
col = basis.reshape(-1, ncomp)[:, component[dof_f]]
65+
nz = np.nonzero(np.abs(col) > 1e-13)[0]
66+
rows.extend([dof_f] * len(nz))
67+
cols.extend(dofs_c[nz])
68+
vals.extend(col[nz])
4569
self._Pmat = sp.csr_matrix((vals, (rows, cols)), shape=(Vf.dim(), Vc.dim()))
4670
return self._Pmat
4771

@@ -90,6 +114,23 @@ def project(self, F):
90114
not an alternative here -- high-order Lagrange basis functions are not positive, so the
91115
mass row sums can vanish, and it failed to converge at all.
92116
117+
For DG this samples a discontinuous function at coarse dof points, most of which sit on a
118+
fine facet where it has two values -- 9 of 17 for DG4 under bisection, all of them for DG1
119+
and DG2. Point sampling is not merely mis-implemented there, it is not a well-defined
120+
operator. This is left as it is on purpose, and the reason is worth keeping straight,
121+
because the same ambiguity in prolong() was fatal:
122+
123+
R_u acts on the SOLUTION, whose jumps are the DG discretisation error, O(h^(p+1)). Measured
124+
on the burgers example: against the exact L2 projection M_c^-1 P^T M_f, sampling differs by
125+
4.4 on a random (genuinely jumpy) state of size 3.9, and by 9e-12 on the states the solver
126+
actually visits. Swapping in the L2 projection changes not one iteration count, at nu = 0.02
127+
or at nu = 0.002 where the front is ten times steeper. P acts on the coarse CORRECTION, whose
128+
jumps are O(1) relative to itself -- which is why continuising it deleted the coarse level.
129+
130+
So this only bites on a solution whose own jumps are O(1): an under-resolved shock, or a
131+
limited state. If you get there, the exact operator is M_c^-1 P^T M_f, one coarse mass solve,
132+
block-diagonal for DG.
133+
93134
Args:
94135
F: the fine level data
95136
"""
@@ -115,17 +156,22 @@ def restrict(self, F):
115156

116157
def prolong(self, G):
117158
"""
118-
Prolongation implementation
159+
Prolongation implementation, the exact inclusion via P.
160+
161+
Not df.interpolate: see the Pmat docstring for why that silently continuises a
162+
discontinuous coarse function.
119163
120164
Args:
121165
G: the coarse level data
122166
"""
167+
P = self.Pmat
123168
if isinstance(G, fenics_mesh):
124-
u_fine = fenics_mesh(df.interpolate(G.values, self.fine_prob.init))
169+
u_fine = fenics_mesh(self.fine_prob.init)
170+
u_fine.values.vector()[:] = P.dot(G.values.vector()[:])
125171
elif isinstance(G, rhs_fenics_mesh):
126172
u_fine = rhs_fenics_mesh(self.fine_prob.init)
127-
u_fine.impl.values = df.interpolate(G.impl.values, self.fine_prob.init)
128-
u_fine.expl.values = df.interpolate(G.expl.values, self.fine_prob.init)
173+
u_fine.impl.values.vector()[:] = P.dot(G.impl.values.vector()[:])
174+
u_fine.expl.values.vector()[:] = P.dot(G.expl.values.vector()[:])
129175
else:
130176
raise TransferError('Unknown type of coarse data, got %s' % type(G))
131177

pySDC/projects/FEniCS_MLSDC/README.rst

Lines changed: 145 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ This is the reference for combining pySDC with finite elements. It shows SDC, ML
55
three FEniCS problems, using the **mass-matrix formulation throughout** -- the mass matrix is never
66
inverted, anywhere.
77

8+
Each problem comes with continuous (``CG``) and discontinuous (``DG``) elements, and each hierarchy
9+
can be coarsened in either direction: a coarser mesh at fixed element order (**h**), or a lower
10+
element order on the same mesh (**p**).
11+
812
What is here
913
------------
1014

@@ -15,8 +19,16 @@ What is here
1519
- ``burgers`` -- viscous Burgers in 1D, fully implicit with a node-local Newton.
1620
- ``grayscott`` -- Gray-Scott reaction-diffusion in 1D, fully implicit with a node-local Newton.
1721

22+
``get_description(example, nlevels, family, coarsening)`` builds any of the twelve combinations.
23+
24+
``problem_classes/DG_1D_FEniCS.py``
25+
The DG counterparts of all three: interior penalty for diffusion, Nitsche for Dirichlet data,
26+
a Lax-Friedrichs flux for the Burgers advection. Each is its CG parent with the weak form
27+
replaced -- the Newton loop, the mass matrix and the solver interface are inherited unchanged.
28+
1829
``run_examples.py``
19-
Runs each example with SDC, MLSDC on 2 and 3 levels, and PFASST on up to 8 parallel steps.
30+
Runs every example, family and coarsening direction with SDC, MLSDC on 2 and 3 levels, and
31+
PFASST on up to 8 parallel steps. Takes about four minutes.
2032

2133
``tests/``
2234
Asserts the claims below, so they stay true.
@@ -41,18 +53,22 @@ and a problem class whose ``eval_f`` returns the assembled weak form rather than
4153
:math:`M^{-1}F`, whose ``solve_system`` takes a right-hand side that is already in the dual space,
4254
and which implements ``apply_mass_matrix``.
4355

44-
Coarsen by **mesh refinement** and keep the collocation nodes on every level.
56+
Then: **high order, coarsened in h.** CG or DG, they perform identically. The rest of this file is
57+
why, and what it took to make the DG half true.
58+
59+
Use high-order elements
60+
-----------------------
4561

46-
**Use high-order elements.** This is not a detail: it is what makes the multilevel hierarchy pay at
47-
all. At *identical* fine-level dof counts, with only the element order changed:
62+
This is not a detail: it is what makes the multilevel hierarchy pay at all. At *identical*
63+
fine-level dof counts, with only the element order changed:
4864

49-
============= ============== ============== ==============
50-
example CG1 CG2 CG4
51-
============= ============== ============== ==============
52-
``heat`` 0.92x / 1.05x 1.22x / 1.05x 1.22x / 1.57x
53-
``burgers`` 0.89x / 0.76x 0.94x / 0.81x 1.26x / 1.94x
54-
``grayscott`` 0.87x / 0.74x 0.87x / 0.74x 1.24x / 1.24x
55-
============= ============== ============== ==============
65+
============= ============= ============= =============
66+
example CG1 CG2 CG4
67+
============= ============= ============= =============
68+
``heat`` 0.92x / 1.05x 1.22x / 1.05x 1.22x / 1.57x
69+
``burgers`` 0.89x / 0.76x 0.94x / 0.81x 1.26x / 1.94x
70+
``grayscott`` 0.87x / 0.74x 0.87x / 0.74x 1.24x / 1.24x
71+
============= ============= ============= =============
5672

5773
(speed-up at 2 / 3 levels; below 1.00x means MLSDC costs more than SDC)
5874

@@ -66,10 +82,109 @@ The natural prolongation between nested finite element spaces **is** the inclusi
6682
approximation order is the element order. So "use CG4" here and "use ``iorder=6``" there are the same
6783
statement about the same operator.
6884

85+
Coarsen in h, not in p
86+
----------------------
87+
88+
The same statement, pointed at the coarsening direction. Both ladders are nested, and with CG both
89+
halve the dof count per level -- ``CG4`` on meshes of 512/256/128 cells against ``CG4/CG2/CG1`` on
90+
512 cells give the identical 2049/1025/513 -- so for CG the two cost exactly the same per iteration
91+
and only the quality of the coarse space differs. (With DG the p ladder cannot halve: order 4/2/1
92+
means 5/3/2 dofs per cell, so p-coarsening is charged more there as well.)
93+
94+
============== ========== ============== ==============
95+
config SDC (work) MLSDC 2 levels MLSDC 3 levels
96+
============== ========== ============== ==============
97+
heat CG h 5.75 4.50 (1.28x) 3.50 (1.64x)
98+
heat DG h 5.75 4.50 (1.28x) 3.50 (1.64x)
99+
heat CG p 5.75 4.50 (1.28x) 5.25 (1.09x)
100+
heat DG p 5.75 4.80 (1.20x) 6.00 (0.96x)
101+
burgers CG h 4.12 3.19 (1.29x) 1.97 (2.09x)
102+
burgers DG h 4.12 3.19 (1.29x) 1.97 (2.10x)
103+
burgers CG p 4.12 4.50 (0.92x) 5.25 (0.79x)
104+
burgers DG p 4.12 4.80 (0.86x) 6.00 (0.69x)
105+
grayscott CG h 6.50 5.44 (1.20x) 5.47 (1.19x)
106+
grayscott DG h 6.50 5.44 (1.20x) 5.47 (1.19x)
107+
grayscott CG p 6.50 7.50 (0.87x) 8.76 (0.74x)
108+
grayscott DG p 6.50 8.00 (0.81x) 10.00 (0.65x)
109+
============== ========== ============== ==============
110+
111+
(work = iterations x summed dof ratio, so every level is charged for what it costs)
112+
113+
h-coarsening wins in every one of the six cases. The reason is the one above: dropping from ``CG4``
114+
to ``CG2`` to ``CG1`` on a fixed mesh leaves a coarse space with :math:`O(h^2)` approximation error,
115+
while keeping ``CG4`` and doubling the cell size gives :math:`O((2h)^5)`. The coarse level is there
116+
to resolve the smooth part of the error, and a low order resolves it badly however fine the mesh.
117+
118+
Note also that p-coarsening never gets past the second level: two levels are close to h-coarsening,
119+
three are worse than two. ``CG2`` still approximates the smooth error; ``CG1`` does not.
120+
121+
CG or DG
122+
--------
123+
124+
Identical, once the DG hierarchy is built correctly. Same iteration counts, same speed-ups, same
125+
PFASST growth, on all three examples -- read the table above in pairs.
126+
127+
That took two fixes, both of which CG gets for free and neither of which shows up in a
128+
discretisation test. Both are in the defect list below, items 5 and 6. Before them, DG looked like a
129+
dead end:
130+
131+
========================= ================ ================== ================
132+
DG, h-coarsening broken hierarchy Galerkin hierarchy CG for reference
133+
========================= ================ ================== ================
134+
heat, 3 levels 1.10x 1.64x 1.64x
135+
burgers, 3 levels 0.79x 2.10x 2.09x
136+
grayscott, 3 levels 0.65x 1.19x 1.19x
137+
heat, PFASST 8 steps 12.88 iters 4.62 iters 4.62 iters
138+
burgers, PFASST 8 steps 13.50 iters 4.12 iters 4.12 iters
139+
grayscott, PFASST 8 steps did not converge 6.75 iters 6.00 iters
140+
========================= ================ ================== ================
141+
142+
The one honest difference that remains: at the same mesh and order DG carries 25% more dofs (5 per
143+
cell against 4) for the same accuracy, because these solutions are smooth. Same iterations, more
144+
work per iteration. DG earns those dofs on discontinuities and on advection-dominated transport,
145+
which none of these examples have -- so use it here only if you want it for other reasons, and know
146+
that the multilevel machinery will not hold you back.
147+
148+
The penalty constant :math:`\sigma` is not a tuning knob. Sweeping it over 1, 2, 5, 10, 40 changes
149+
nothing above the coercivity threshold; only :math:`\sigma = 1` sits below it and wrecks the coarse
150+
correction. Set it just clear of the threshold and stop thinking about it. What matters is not how
151+
big it is but that it is *the same on every level*, which is item 6.
152+
153+
What it demonstrates
154+
--------------------
155+
156+
**Savings** -- see the table above. A third level pays clearly on the smooth problems and is neutral
157+
on Gray-Scott. Expect less from a multilevel hierarchy the more nonlinear the problem is.
158+
159+
**Stability** -- PFASST iteration counts as parallel steps are added, every run agreeing with serial
160+
to the tolerance of the example:
161+
162+
============== ====== ==== ==== =====
163+
config 1 step 2 4 8
164+
============== ====== ==== ==== =====
165+
heat CG h 3.00 3.38 3.88 4.62
166+
heat DG h 3.00 3.38 3.88 4.62
167+
heat CG p 3.00 3.50 4.00 4.75
168+
heat DG p 3.00 3.50 4.00 4.75
169+
burgers CG h 2.12 2.62 3.25 4.12
170+
burgers DG h 2.12 2.62 3.25 4.12
171+
burgers CG p 3.00 3.00 3.38 4.12
172+
burgers DG p 3.00 3.00 3.38 4.12
173+
grayscott CG h 3.62 3.88 4.50 6.00
174+
grayscott DG h 3.62 4.00 4.75 6.75
175+
grayscott CG p 5.00 5.62 6.88 9.25
176+
grayscott DG p 5.00 6.00 8.12 12.12
177+
============== ====== ==== ==== =====
178+
179+
Growth out to 8 parallel steps is 1.4-2.4x, which is what PFASST is supposed to do.
180+
69181
Why earlier attempts did not pay off
70182
------------------------------------
71183

72-
Four separate defects, each of which quietly capped or broke the multilevel gain:
184+
Six separate defects, each of which quietly capped or broke the multilevel gain. Note what they have
185+
in common: every one of them leaves a method that still converges, still to the right answer, with a
186+
coarse level that corrects far less than it should. None of them is visible in a discretisation
187+
test, and none of them raises anything.
73188

74189
1. **The FAS** :math:`\tau` **was restricted by interpolation.** :math:`\tau` is a load vector, not a
75190
nodal function, so it has to be restricted with :math:`P^T`. Interpolating it is wrong by roughly
@@ -82,38 +197,27 @@ Four separate defects, each of which quietly capped or broke the multilevel gain
82197
asymptotically inert -- it can neither help nor hurt, and it masks a broken transfer. Combined
83198
with ``Problem.apply_mass_matrix`` silently defaulting to the identity, a wrong mass matrix
84199
produced a stalled iteration many sweeps later rather than an error.
200+
5. **The prolongation was not the inclusion, for DG.** ``mesh_to_mesh_fenics`` built :math:`P` with
201+
``df.interpolate``. Across meshes that is point evaluation, and a fine dof sitting *on* a coarse
202+
facet has two coarse values there -- dolfin returns whichever cell the bounding-box tree finds
203+
first. The prolonged function is therefore continuous at every coarse facet: the jumps, which are
204+
the whole point of a DG space, are deleted. The error is :math:`O(1)` in the jump and exactly
205+
zero for smooth data, which is why it survived a convergence test at
206+
:math:`O(h^{p+1})`. :math:`P` is now assembled cell by cell, evaluating the coarse basis in the
207+
coarse cell that *contains* each fine cell. For continuous spaces this reproduces the old
208+
construction to machine precision.
209+
6. **The interior penalty was rediscretised on every level.** The CG bilinear form does not know
210+
which mesh it lives on, so rediscretising it on a coarse level gives exactly the Galerkin
211+
operator :math:`P^T A_F P`. The SIPG form does know: its penalty scales as
212+
:math:`\sigma p^2 / h`, so a coarser mesh halves it and a lower order divides it by
213+
:math:`(p_f/p_c)^2`. That is not a small perturbation -- the penalty outweighs the volume term by
214+
:math:`\sigma p^2`, so the coarse operator was wrong in its *dominant* term and corrected almost
215+
nothing. ``setups.py`` now pins :math:`\alpha_l = \sigma p_0^2 \, h_l / h_0` so that
216+
:math:`\alpha_l / h_l` is the same on every level. With that and item 5,
217+
:math:`A_G = P^T A_F P` holds to machine precision, for both coarsening directions.
85218

86219
Nodes are kept on every level here for a further reason: partial node coarsening (5 -> 4, 5 -> 3) is
87220
worse than no coarse level at all, because it destroys the stiff-limit annihilation that the ``LU``
88221
preconditioner provides.
89222

90-
What it demonstrates
91-
--------------------
92-
93-
**Savings** (work = iterations x summed dof ratio, so a coarse level is charged for what it costs):
94-
95-
============= ========== ========== ==========
96-
example SDC MLSDC (2) MLSDC (3)
97-
============= ========== ========== ==========
98-
``heat`` 5.75 3.00 2.00
99-
speed-up 1.00x 1.28x **1.64x**
100-
``burgers`` 4.12 2.12 1.12
101-
speed-up 1.00x 1.29x **2.09x**
102-
``grayscott`` 6.50 3.62 3.12
103-
speed-up 1.00x 1.20x 1.19x
104-
============= ========== ========== ==========
105-
106-
A third level pays clearly on the smooth problems and is neutral on Gray-Scott. Expect less from a
107-
multilevel hierarchy the more nonlinear the problem is.
108-
109-
**Stability** -- PFASST iteration counts as parallel steps are added, every run agreeing with serial:
110-
111-
============= ====== ====== ====== ======
112-
example 1 step 2 4 8
113-
============= ====== ====== ====== ======
114-
``heat`` 3.00 3.38 3.88 4.62
115-
``burgers`` 2.12 2.62 3.25 4.12
116-
``grayscott`` 3.62 3.88 4.50 6.00
117-
============= ====== ====== ====== ======
118-
119223
Settings are sized for CI runtime, not for a production run. Reproduce with ``run_examples.py``.

0 commit comments

Comments
 (0)