11.4. HDG Tricks (scalar)#
In this unit we want to discuss a few advanced tricks that can be used for further tuning of the
We want to treat:
projected jumps
🫣
hidden
dofsHybrid DG formulations based on lifting operators
For ease of discussion we will first start with a discussion of the first three features in the context of the Poisson problem
and will apply it directly afterwards to the Stokes problem.
Note
We will partially repeat the introduction of the projected jumps approach from the advanced tutorials here in order to provide a self-contained discussion and keep a consistent notation. Furthermore we will give more details on the tricks behind the curtain.
11.4.1. Preliminary: Recap of standard HDG #
The standard interior penalty DG method for the Poisson problem takes the form:
(discontinuous pol. on elements) (discontinuous pol. on facets), solves for all withHDG bilinear form
:HDG linear form
: .
Show code cell source
# input : fes = V x Vhat:
def setup_HDG_system(fes, **opts):
uh, vh = fes.TnT()
(u,uhat), (v,vhat) = uh, vh
jump = lambda uh: uh[0]-uh[1]
order = fes.components[0].globalorder
alpha = 2
h = specialcf.mesh_size
n = specialcf.normal(mesh.dim)
a = BilinearForm(fes, **opts)
dS = dx(element_boundary=True)
a += grad(u)*grad(v)*dx + \
alpha*(order+1)**2/h*jump(uh)*jump(vh)*dS + \
(-grad(u)*n*jump(vh) - grad(v)*n*jump(uh))*dS
a.Assemble()
f = LinearForm(fes)
f += 5*v*dx
f.Assemble()
return a,f
Show code cell source
def solve_lin_system(a,f,gfu,inverse=""):
aSinv = a.mat.Inverse(gfu.space.FreeDofs(coupling=a.condense),inverse=inverse)
if a.condense:
ainv = ((IdentityMatrix(a.mat.height) + a.harmonic_extension) @ (aSinv + a.inner_solve) @ (IdentityMatrix(a.mat.height) + a.harmonic_extension_trans))
else:
ainv = aSinv
rhs = f.vec.CreateVector()
rhs.data = f.vec - a.mat * gfu.vec
gfu.vec.data += ainv * rhs
from ngsolve import *
from ngsolve.webgui import Draw
from netgen.occ import *
mesh = Mesh(unit_square.GenerateMesh(maxh=0.1))
order=1
V = L2(mesh, order=order)
F = FacetFESpace(mesh, order=order, dirichlet=".*")
fes = V*F
gfu = GridFunction(fes)
a,f, = setup_HDG_system(fes, condense=True)
solve_lin_system(a,f,gfu)
Draw (gfu.components[0], mesh, "u-HDG", deformation=True);
11.4.2. Projected Jumps (and how it works)#
An observation:
on every facet ,hence
where is the projection on
We then apply the following modifications:
To bound the
-terms it hence suffices to reduce the stabilizing term toWe hence reduce the facet space to
(which reduces the globally coupleddofs
, especially for low order methods)
Now the questions arises: How to realize the ``projected jumps’’
11.4.3. highest_order_dc
: Highest order facet functions discontinuous#
The basis functions in FacetFESpace(..., order=l, ...)
in NGSolve
) are constructed
The implementation trick for the ``projected jumps’’ method is to
use formally the same bilinear and linear form as in the standard HDG version,
but choose the following (seemingly more complicated) facet space:

on each facet
the -part of the facet function, , stays uni-valued whilethe
-orthogonal complement in , , becomes double-valued (one version for both aligned elements)
Testing with
Hence, the element-local facet variable becomes
We can hence eliminate
So, in 2D we add one degree of freedom per edge:
Vhat = FacetFESpace(mesh, order=order, dirichlet=".*")
Vhats = FacetFESpace(mesh, order=order, dirichlet=".*", \
highest_order_dc=True)
print(f"Vhat.ndof = {Vhat.ndof}, Vhats.ndof = {Vhats.ndof}, difference = {Vhats.ndof - Vhat.ndof}, number of facets = {mesh.nfacet}")
Vhat.ndof = 718, Vhats.ndof = 1037, difference = 319, number of facets = 359
What is wrong? Make a guess!
Click to show
The highest_order_dc
-flag leaves boundary facets untouched!
Show code cell source
print(f"boundary facets: {mesh.GetNE(BND)}")
boundary facets: 40
We have introduced more unknowns, but
the new unknowns stay element-local and can be condensed out
print(f"Vhat.nfreedofs = {sum(Vhat.FreeDofs(coupling=True))}, Vhats.nfreedofs = {sum(Vhats.FreeDofs(coupling=True))}")
Vhat.nfreedofs = 638, Vhats.nfreedofs = 319
11.4.5. HDG Lifting’s#
As a next application of HIDDEN_DOF
s we consider the lifting operator approach for the DG discretization.
The stability term
The HDG lifting operator
Note that with
With this we obtain:
Now, it is easy to see, that new sufficient stabilizations (without need for a sufficiently large parameter) can be designed easily.
We simply take
Note that the latter part makes it definite. Otherwise even multiples of the former part my not be sufficient if the lifting operator has a non-trivial kernel (due to a non-trivial kernel of
This yields
Now, for the implementation we rewrite the terms using the lifting characteristics backwards:
To implement the second-last part we introduce a new variable
Now, Plugging
def setup_lifted_HDG_system(fes, **opts):
uh, vh = fes.TnT()
order = fes.components[0].globalorder
(u,uhat,ul), (v,vhat,vl) = uh, vh
jump = lambda uh: uh[0]-uh[1]
h = specialcf.mesh_size
n = specialcf.normal(mesh.dim)
a = BilinearForm(fes, **opts)
dS = dx(element_boundary=True)
a += grad(u)*grad(v)*dx # b_h
a += (-grad(u)*n*jump(vh) - grad(v)*n*jump(uh))*dS #N_h
a += 1*(order+1)/h*jump(uh)*jump(vh)*dS # s_h
a += -grad(ul)*grad(vl)*dx # b_h (lifting)
a += (-grad(ul)*n*jump(vh) - grad(vl)*n*jump(uh))*dS #N_h (lifting)
a += - 1/(h*h)* ul * vl * dx(bonus_intorder=-2*order) # j_h
a.Assemble()
f = LinearForm(fes)
f += 5*v*dx
f.Assemble()
return a,f
We note that the new variable HIDDEN
variable as
it only appears on one element
it is only needed during assembly (to realize the lifting)
the r.h.s. of the linear system for
is zero
We use the keyword hide_all_dofs
to make all dofs in a space HIDDEN
:
order=1
V = L2(mesh, order=order)
F = FacetFESpace(mesh, order=order, dirichlet=".*")
#F = Compress(FacetFESpace(mesh, order=order, dirichlet=".*", highest_order_dc=True, hide_highest_order_dc=True))
Vl = Compress(L2(mesh, order=order, hide_all_dofs=True))
fes = V*F*Vl
Is Vl
empty now?
Yes and no:
print(f"global ndof: {Vl.ndof}")
for el in Vl.Elements():
print(f"local dofs on first element: {el.dofs}")
break
global ndof: 0
local dofs on first element: [-2, -2, -2]
Warning
Setting dof
s as HIDDEN_DOF
s changes the global number of dof
s and the global numbering (after compression), but
on the element level the number of dof
s is unaffected! The computation of the element matrix (before applying Schur complement strategies) is independent of any information on coupling types in the FESpace
.
gfuT_vis = GridFunction(V, multidim=True)
gfu = GridFunction(fes)
for i, (opt, elims) in enumerate(reversed(linsys_opts)):
a,f, = setup_lifted_HDG_system(fes, **opt)
solve_lin_system(a,f,gfu)
print(f"condensed dofs: {elims:6}, nzes: {a.mat.nze}")
if i == 0:
gfuT_vis.vec.data = gfu.components[0].vec
else:
gfuT_vis.AddMultiDimComponent(gfu.components[0].vec)
Draw (gfuT_vis, mesh, "u-HDG", deformation=True);
condensed dofs: local, nzes: 6860
condensed dofs: hidden, nzes: 17030
condensed dofs: none, nzes: 17030
Warning
Recall: Don’t use HIDDEN_DOF
s + Compress
without setting condense=True
or eliminate_hidden=True
in the BilinearForm
!
Note
Also here for the chosen lifting formulation we don’t need to penalize jumps up to order