This page was generated from unit-2.1.3-bddc/bddc.ipynb.

2.1.3 Element-wise BDDC Preconditioner

The element-wise BDDC (Balancing Domain Decomposition preconditioner with Constraints) preconditioner in NGSolve is a good general purpose preconditioner that works well both in the shared memory parallel mode as well as in distributed memory mode. In this tutorial, we discuss this preconditioner, related built-in options, and customization from python.

Let us start with a simple description of the element-wise BDDC in the context of Lagrange finite element space \(V\). Here the BDDC preconditoner is constructed on an auxiliary space \(\widetilde{V}\) obtained by connecting only element vertices (leaving edge and face shape functions discontinuous). Although larger, the auxiliary space allows local elimination of edge and face variables. Hence an analogue of the original matrix \(A\) on this space, named \(\widetilde A\), is less expensive to invert. This inverse is used to construct a preconditioner for \(A\) as follows:

\[C_{BDDC}^{-1} = R {\,\widetilde{A}\,}^{-1}\, R^t\]

Here, \(R\) is the averaging operator for the discontinous edge and face variables.

To construct a general purpose BDDC preconditioner, NGSolve generalizes this idea to all its finite element spaces by a classification of degrees of freedom. Dofs are classified into (condensable) LOCAL_DOFs that we saw in 1.4 and a remainder that includes these types:

WIREBASKET_DOF
INTERFACE_DOF

The original finite element space \(V\) is obtained by requiring conformity of both the above types of dofs, while the auxiliary space \(\widetilde{V}\) is obtained by requiring conformity of WIREBASKET_DOFs only.

Here is a figure of a typical function in the default \(\widetilde{V}\) (and the code to generate this is at the end of this tutorial) when \(V\) is the Lagrange space:

title

[1]:
import netgen.gui
from ngsolve import *
from ngsolve.la import EigenValues_Preconditioner
from netgen.csg import unit_cube
from netgen.geom2d import unit_square
SetHeapSize(100*1000*1000)
[2]:
mesh = Mesh(unit_cube.GenerateMesh(maxh=0.5))
# mesh = Mesh(unit_square.GenerateMesh(maxh=0.5))

Built-in options

Let us define a simple function to study how the spectrum of the preconditioned matrix changes with various options.

Effect of condensation

[3]:
def TestPreconditioner (p, condense=False, **args):
    fes = H1(mesh, order=p, **args)
    u,v = fes.TnT()
    a = BilinearForm(fes, eliminate_internal=condense)
    a += grad(u)*grad(v)*dx + u*v*dx
    c = Preconditioner(a, "bddc")
    a.Assemble()
    return EigenValues_Preconditioner(a.mat, c.mat)
[4]:
lams = TestPreconditioner(5)
print (lams[0:3], "...\n", lams[-3:])
 1.00374
 1.10077
 1.29559
 ...
  22.8683
 27.2387
 29.5068

Here is the effect of static condensation on the BDDC preconditioner.

[5]:
lams = TestPreconditioner(5, condense=True)
print (lams[0:3], "...\n", lams[-3:])
 1.00153
 1.04077
 1.13375
 ...
  7.44898
 8.13299
 8.95809

Tuning the auxiliary space

Next, let us study the effect of a few built-in flags for finite element spaces that are useful for tweaking the behavior of the BDDC preconditioner. The effect of these flags varies in two (2D) and three dimensions (3D), e.g.,

  • wb_fulledges=True: This option keeps all edge-dofs conforming (i.e. they are marked WIREBASKET_DOFs). This option is only meaningful in 3D. If used in 2D, the preconditioner becomes a direct solver.

  • wb_withedges=True: This option keeps only the first edge-dof conforming (i.e., the first edge-dof is marked WIREBASKET_DOF and the remaining edge-dofs are marked INTERFACE_DOFs).

The complete situation is a bit more complex due to the fact these options can take the three values True, False, or Undefined, the two options can be combined, and the space dimension can be 2 or 3. The default value of these flags in NGSolve is Undefined. Here is a table with the summary of the effect of these options:

wb_fulledges

wb_withedges

2D

3D

True

any value

all

all

False/Undefined

Undefined

none

first

False/Undefined

False

none

none

False/Undefined

True

first

first

An entry \(X \in\) {all, none, first} of the last two columns is to be read as follows: \(X\) of the edge-dofs is(are) WIREBASKET_DOF(s).

Here is an example of the effect of one of these flag values.

[6]:
lams = TestPreconditioner(5, condense=True,
                          wb_withedges=False)
print (lams[0:3], "...\n", lams[-3:])
 1.00425
 1.08049
 1.32286
 ...
  30.4513
 32.3987
 32.9451

Clearly, when moving from the default case (where the first of the edge dofs are wire basket dofs) to the case (where none of the edge dofs are wire basket dofs), the conditioning became less favorable.

Customize

From within python, we can change the types of degrees of freedom of finite element spaces, thus affecting the behavior of the BDDC preconditioner.

To depart from the default and commit the first two edge dofs to wire basket, we perform the next steps:

[7]:
fes = H1(mesh, order=10)
u,v = fes.TnT()

for ed in mesh.edges:
    dofs = fes.GetDofNrs(ed)
    for d in dofs:
        fes.SetCouplingType(d, COUPLING_TYPE.INTERFACE_DOF)

    # Set the first two edge dofs to be conforming
    fes.SetCouplingType(dofs[0], COUPLING_TYPE.WIREBASKET_DOF)
    fes.SetCouplingType(dofs[1], COUPLING_TYPE.WIREBASKET_DOF)

a = BilinearForm(fes, eliminate_internal=True)
a += grad(u)*grad(v)*dx + u*v*dx
c = Preconditioner(a, "bddc")
a.Assemble()

lams=EigenValues_Preconditioner(a.mat, c.mat)
max(lams)/min(lams)
[7]:
24.44038895906415

This is a slight improvement from the default.

[8]:
lams = TestPreconditioner (10, condense=True)
max(lams)/min(lams)
[8]:
38.06905699705679

Combine BDDC with AMG for large problems

The coarse inverse \({\,\widetilde{A}\,}^{-1}\) of BDDC is expensive on fine meshes. Using the option coarsetype=h1amg flag, we can ask BDDC to replace \({\,\widetilde{A}\,}^{-1}\) by an Algebraic MultiGrid (AMG) preconditioner. Since NGSolve’s h1amg is well-suited
for the lowest order Lagrange space, we use the option wb_withedges=False to ensure that \(\widetilde{A}\) is made solely with vertex unknowns.
[9]:
p = 5
mesh = Mesh(unit_cube.GenerateMesh(maxh=0.05))
fes = H1(mesh, order=p, dirichlet="left|bottom|back",
         wb_withedges=False)
print("NDOF = ", fes.ndof)
u,v = fes.TnT()
a = BilinearForm(fes)
a += grad(u)*grad(v)*dx
f = LinearForm(fes)
f += v*dx

with TaskManager():
    pre = Preconditioner(a, "bddc", coarsetype="h1amg")
    a.Assemble()
    f.Assemble()

    gfu = GridFunction(fes)
    solvers.CG(mat=a.mat, rhs=f.vec, sol=gfu.vec,
               pre=pre, maxsteps=500)
Draw(gfu)
NDOF =  1146726
iteration 0 error = 0.708802616332962
iteration 1 error = 0.2880387511317823
iteration 2 error = 0.2777052954298466
iteration 3 error = 0.28802115597527206
iteration 4 error = 0.3164988880660466
iteration 5 error = 0.2422043046589375
iteration 6 error = 0.1867133286752493
iteration 7 error = 0.15495818469657244
iteration 8 error = 0.1215707584732122
iteration 9 error = 0.09871216409124454
iteration 10 error = 0.07416484530918536
iteration 11 error = 0.05802375878310737
iteration 12 error = 0.047176229204210975
iteration 13 error = 0.03860496800095339
iteration 14 error = 0.031240378784601907
iteration 15 error = 0.024430544031740255
iteration 16 error = 0.018837944758251603
iteration 17 error = 0.015321463448573524
iteration 18 error = 0.01265246657213023
iteration 19 error = 0.009977256307324583
iteration 20 error = 0.0076629563127063
iteration 21 error = 0.00604650445788516
iteration 22 error = 0.004849160002900024
iteration 23 error = 0.0038808045883691787
iteration 24 error = 0.0031159869755667457
iteration 25 error = 0.0024850469941350814
iteration 26 error = 0.002024253458195875
iteration 27 error = 0.0016357764353589512
iteration 28 error = 0.0013030494365250805
iteration 29 error = 0.0010391487389604286
iteration 30 error = 0.0008384547772049022
iteration 31 error = 0.0006795122070749162
iteration 32 error = 0.000545674881031138
iteration 33 error = 0.00043008797472223837
iteration 34 error = 0.00033980120123807507
iteration 35 error = 0.0002736241528840215
iteration 36 error = 0.00022117517545390956
iteration 37 error = 0.0001772558761296199
iteration 38 error = 0.00013893789199119548
iteration 39 error = 0.00011084631248865312
iteration 40 error = 8.892396977116294e-05
iteration 41 error = 7.07418082208451e-05
iteration 42 error = 5.6972764219625654e-05
iteration 43 error = 4.600926412017718e-05
iteration 44 error = 3.7125679237668804e-05
iteration 45 error = 2.981258594850554e-05
iteration 46 error = 2.3663273501349014e-05
iteration 47 error = 1.8937964599754545e-05
iteration 48 error = 1.5753981800398674e-05
iteration 49 error = 1.2649637838707018e-05
iteration 50 error = 1.0034586913787553e-05
iteration 51 error = 8.090254963984528e-06
iteration 52 error = 6.538438231748118e-06
iteration 53 error = 5.214029556525451e-06
iteration 54 error = 4.201981675453949e-06
iteration 55 error = 3.3521775904428683e-06
iteration 56 error = 2.723618065384638e-06
iteration 57 error = 2.1858194459795013e-06
iteration 58 error = 1.7606523917727036e-06
iteration 59 error = 1.408294912735294e-06
iteration 60 error = 1.1415548402622235e-06
iteration 61 error = 9.300895296900903e-07
iteration 62 error = 7.445918016939429e-07
iteration 63 error = 5.947741164974426e-07
iteration 64 error = 4.773915347835032e-07
iteration 65 error = 3.8284243666610147e-07
iteration 66 error = 3.050932689356518e-07
iteration 67 error = 2.424708824416205e-07
iteration 68 error = 1.9624167710545506e-07
iteration 69 error = 1.5727756692190836e-07
iteration 70 error = 1.2549381377106977e-07
iteration 71 error = 1.0086444917963951e-07
iteration 72 error = 8.087704699044731e-08
iteration 73 error = 6.500442715932302e-08
iteration 74 error = 5.189395607780479e-08
iteration 75 error = 4.221651204800037e-08
iteration 76 error = 3.397219877108467e-08
iteration 77 error = 2.765366088652943e-08
iteration 78 error = 2.2765532445269828e-08
iteration 79 error = 1.874458710661954e-08
iteration 80 error = 1.4895344746033563e-08
iteration 81 error = 1.1984052256575023e-08
iteration 82 error = 9.583339165358251e-09
iteration 83 error = 7.616675689108935e-09
iteration 84 error = 6.119983593405796e-09
iteration 85 error = 4.890455622504068e-09
iteration 86 error = 3.951059607060772e-09
iteration 87 error = 3.1806350362650013e-09
iteration 88 error = 2.50601366825419e-09
iteration 89 error = 1.989847862178305e-09
iteration 90 error = 1.6083027369651805e-09
iteration 91 error = 1.299830941579991e-09
iteration 92 error = 1.0364080807145098e-09
iteration 93 error = 8.397852053828697e-10
iteration 94 error = 6.695899301837766e-10
iteration 95 error = 5.406177154551314e-10
iteration 96 error = 4.3188478945689745e-10
iteration 97 error = 3.4122501485451055e-10
iteration 98 error = 2.7606453975262537e-10
iteration 99 error = 2.2012138528505e-10
iteration 100 error = 1.760527287851968e-10
iteration 101 error = 1.4122361398347346e-10
iteration 102 error = 1.1463482064096631e-10
iteration 103 error = 9.159091227546788e-11
iteration 104 error = 7.346532487660465e-11
iteration 105 error = 5.832238410834527e-11
iteration 106 error = 4.6391131189341327e-11
iteration 107 error = 3.744490884312188e-11
iteration 108 error = 2.988372964733583e-11
iteration 109 error = 2.401963194439444e-11
iteration 110 error = 1.9280313810731813e-11
iteration 111 error = 1.5443704160843748e-11
iteration 112 error = 1.2366063484388644e-11
iteration 113 error = 1.0040787502064784e-11
iteration 114 error = 7.987408043600644e-12
iteration 115 error = 6.385289269036176e-12
iteration 116 error = 5.100429130408614e-12
iteration 117 error = 4.041519354422347e-12
iteration 118 error = 3.2788841030179077e-12
iteration 119 error = 2.694998096169187e-12
iteration 120 error = 2.1912956422173505e-12
iteration 121 error = 1.7699240352432365e-12
iteration 122 error = 1.4071487521498079e-12
iteration 123 error = 1.1128137593127099e-12
iteration 124 error = 8.931765661600832e-13
iteration 125 error = 7.19031763903001e-13
iteration 126 error = 5.745697056280145e-13

Postscript

By popular demand, here is the code to draw the figure found at the beginning of this tutorial:

[10]:
from netgen.geom2d import unit_square
mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))
fes_ho = Discontinuous(H1(mesh, order=10))
fes_lo = H1(mesh, order=1, dirichlet=".*")
fes_lam = Discontinuous(H1(mesh, order=1))
fes = FESpace([fes_ho, fes_lo, fes_lam])
uho, ulo, lam = fes.TrialFunction()

a = BilinearForm(fes)
a += Variation(0.5 * grad(uho)*grad(uho)*dx
               - 1*uho*dx
               + (uho-ulo)*lam*dx(element_vb=BBND))
gfu = GridFunction(fes)
solvers.Newton(a=a, u=gfu)
Draw(gfu.components[0])
Newton iteration  0
err =  0.3929793997717297
Newton iteration  1
err =  3.956677249565376e-15