Biharmonic equation
Authors: Julius Herb, Ottar Hellan and Jørgen S. Dokken
Download sources
This demo illustrates how to:
Solve a linear partial differential equation
Use a discontinuous Galerkin method
Solve a fourth-order differential equation
Use the method of manufactured solutions
Equation and problem definition
Strong formulation
The biharmonic equation is a fourth-order elliptic equation. On the domain \(\Omega \subset \mathbb{R}^{d}\), \(1 \le d \le 3\), it reads
where \(\nabla^{4} \equiv \nabla^{2} \nabla^{2}=\Delta\Delta\) is the biharmonic operator and \(f\) is a prescribed source term. To formulate a complete boundary value problem, the biharmonic equation must be complemented by suitable boundary conditions.
Choice of boundary conditions
As we have a fourth order partial differential equation, we are required to supply two boundary conditions. There are two common sets of conditions that people use for the biharmonic equation, namely the clamped condition and the simply supported condition, see for instance Gander and Liu [GL17].
In this demo we will consider the clamped boundary conditions
as the simply supported boundary conditions reduces the system into two sequential Poisson problems, named the Ciarlet-Raviart formulation [CR74].
Weak formulation
Multiplying the biharmonic equation by a test function and integrating by parts twice leads to a problem of second-order derivatives, which would require \(H^{2}\) conforming (roughly \(C^{1}\) continuous) basis functions. To solve the biharmonic equation using Lagrange finite element basis functions, the biharmonic equation can be split into two second- order equations (see the Mixed Poisson demo for an example of a mixed method), or a variational formulation can be constructed that imposes weak continuity of normal derivatives between finite element cells. This demo uses a discontinuous Galerkin approach to impose continuity of the normal derivative weakly, see for instance Babuška and Zlámal [BZ73] or Georgoulis and Houston [GH09].
In this demo, we consider equation 3.20-3.21 from Georgoulis and Houston [GH09], a \(\mathcal{C}^0\)-interior penalty method. However, instead of using a broken (discontinuous) finite element space for the unknown \(u\), we use a continuous space, which simplifies the formulation to a weak formulation of the biharmonic problem reads: find \(u \in V_{g_D}\) such that
where the bilinear form is
and the linear form is
Furthermore, \(\left< u \right> = \frac{1}{2} (u_{+} + u_{-})\), \([\!\![ w ]\!\!] = w_{+} \cdot n_{+} + w_{-} \cdot n_{-}\), \(\beta \ge 0\) is a penalty parameter and \(h_E\) is a measure of the cell size. We use the penalty parameter described in Bringmann et al. [BCS24].
where \(K\) is an element of the mesh, while \(\mathcal{E}_h^{\rm int}\) is the collection of all interior facets, while \(\mathcal{E}_h^{\rm ext}\) is the set of exterior facets.
Note
Note that the Dirichlet condition for \(u\) will be enforced strongly in this example.
Implementation
We follow the example of Georgoulis and Houston [GH09] and use the method of manufactured solutions to construct a \(f\), \(g_D\), \(g_N\) that satisfies
We first import the modules and functions that the program uses:
from pathlib import Path
from mpi4py import MPI
import numpy as np
import ufl
from dolfinx import default_real_type, default_scalar_type, fem, has_adios2, io, mesh, plot
from dolfinx.fem.petsc import LinearProblem
from dolfinx.mesh import CellType, GhostMode
if np.issubdtype(default_real_type, np.float32): # type: ignore
print("float32 not yet supported for this demo.")
exit(0)
We begin by using create_rectangle to create a rectangular
Mesh of the domain, and creating a
finite element FunctionSpace
\(V\) on the mesh.
N = 32
msh = mesh.create_unit_square(
MPI.COMM_WORLD,
N,
N,
cell_type=CellType.triangle,
ghost_mode=GhostMode.shared_facet,
)
As noted in Georgoulis and Houston [GH09] second order Lagrange elements yield sub-optimal convergence, and therefore we choose to use third order elements
degree = 3
V = fem.functionspace(msh, ("Lagrange", degree))
The second argument to functionspace is a tuple consisting of (family, degree), where family is the finite element family, and degree
specifies the polynomial degree. in this case V consists of
third-order, continuous Lagrange finite element functions.
For further details of how one can specify
finite elements as tuples, see ElementMetaData.
Next, we locate the mesh facets that lie on the boundary
\(\Gamma_D = \partial\Omega\).
We do this using using exterior_facet_indices which returns all mesh boundary
facets (Note: if we are only interested in a subset of those, consider
locate_entities_boundary).
tdim = msh.topology.dim
msh.topology.create_connectivity(tdim - 1, tdim)
facets = mesh.exterior_facet_indices(msh.topology)
We now find the degrees-of-freedom that are associated with the
boundary facets using locate_dofs_topological
fdim = tdim - 1
dofs = fem.locate_dofs_topological(V=V, entity_dim=fdim, entities=facets)
and use dirichletbc to create a
DirichletBC
class that represents the boundary condition.
We define the manufactured solution and interpolate it into the function space of our unknown to apply it as a strong boundary condition
x = ufl.SpatialCoordinate(msh)
u_ex = ufl.sin(2 * ufl.pi * x[0]) ** 2 * ufl.sin(2 * ufl.pi * x[1]) ** 2
g_D_expr = fem.Expression(u_ex, V.element.interpolation_points)
g_D = fem.Function(V)
g_D.interpolate(g_D_expr)
bc = fem.dirichletbc(value=g_D, dofs=dofs)
Next, we express the variational problem using UFL.
First, the penalty parameter \(\beta\) is defined. In addition, we define
a variable h for the cell diameter \(h_E\), a variable nfor the
outward-facing normal vector \(n\) and a variable h_avg for the
average size of cells sharing a facet
\(\left< h \right> = \frac{1}{2} (h_{+} + h_{-})\). Here, the UFL syntax
('+') and ('-') restricts a function to the ('+') and ('-')
sides of a facet.
a = fem.Constant(msh, default_scalar_type(4.0))
vol = ufl.CellVolume(msh)
h = ufl.CellDiameter(msh)
beta = 3.0 * a * degree * (degree - 1) / 8.0 * h("+") ** 2 * ufl.avg(1 / vol)
beta_boundary = 3.0 * a * degree * (degree - 1) * h**2 / vol
n = ufl.FacetNormal(msh)
h_avg = (h("+") + h("-")) / 2.0
After that, we can define the variational problem consisting of the
bilinear form \(a\) and the linear form \(L\). We use ufl to derive
the manufactured \(f\) and \(g_N\).
Note that with dS, integration is carried out over all the interior
facets \(\mathcal{E}_h^{\rm int}\), whereas with ds it would be only
the facets on the boundary of the domain, i.e. \(\partial\Omega\).
The jump operator
\([\!\![ w ]\!\!] = w_{+} \cdot n_{+} + w_{-} \cdot n_{-}\) w.r.t. the
outward-facing normal vector \(n\) is in UFL available as jump(w, n).
# Define variational problem
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
f = ufl.div(ufl.grad(ufl.div(ufl.grad(u_ex))))
g_N = ufl.dot(ufl.grad(u_ex), n)
a = (
ufl.inner(ufl.div(ufl.grad(u)), ufl.div(ufl.grad(v))) * ufl.dx
- ufl.inner(ufl.avg(ufl.div(ufl.grad(u))), ufl.jump(ufl.grad(v), n)) * ufl.dS
- ufl.inner(ufl.jump(ufl.grad(u), n), ufl.avg(ufl.div(ufl.grad(v)))) * ufl.dS
+ beta / h_avg * ufl.inner(ufl.jump(ufl.grad(u), n), ufl.jump(ufl.grad(v), n)) * ufl.dS
- ufl.inner(ufl.div(ufl.grad(u)), ufl.dot(ufl.grad(v), n)) * ufl.ds
- ufl.inner(ufl.dot(ufl.grad(u), n), ufl.div(ufl.grad(v))) * ufl.ds
+ beta_boundary / h * ufl.inner(ufl.dot(ufl.grad(u), n), ufl.dot(ufl.grad(v), n)) * ufl.ds
)
L = (
ufl.inner(f, v) * ufl.dx
- ufl.inner(g_N, ufl.div(ufl.grad(v))) * ufl.ds
+ beta_boundary / h * ufl.inner(g_N, ufl.dot(ufl.grad(v), n)) * ufl.ds
)
We create a LinearProblem
object that brings together the variational problem, the Dirichlet
boundary condition, and which specifies the linear solver. In this
case we use a direct (LU) solver. The solve will compute a solution.
problem = LinearProblem(
a,
L,
bcs=[bc],
petsc_options_prefix="demo_biharmonic_",
petsc_options={
"ksp_type": "preonly",
"pc_type": "lu",
},
)
uh = problem.solve()
assert isinstance(uh, fem.Function)
assert problem.solver.getConvergedReason() > 0
We compute the relative \(L^2\)-error between the computed and exact solution:
error = fem.form(ufl.inner(uh - g_D, uh - g_D) * ufl.dx)
local_error = fem.assemble_scalar(error)
glob_error = error.mesh.comm.allreduce(local_error, op=MPI.SUM)
ref_scale = fem.form(ufl.inner(u_ex, u_ex) * ufl.dx)
local_scale = fem.assemble_scalar(ref_scale)
scale = ref_scale.mesh.comm.allreduce(local_scale, op=MPI.SUM)
print("||u_h-u_ex||_{L^2}^2/||u_ex||_L^2^2: ", f"{glob_error / scale:.5e}")
assert glob_error.real / scale.real < 1e-6
The solution can be written to a VTX-file using VTXWriter which can be opened with ParaView
if has_adios2:
out_folder = Path("out_biharmonic")
out_folder.mkdir(parents=True, exist_ok=True)
with io.VTXWriter(msh.comm, out_folder / "biharmonic.bp", [uh]) as file:
file.write(0.0)
and displayed using pyvista.
try:
import pyvista
cells, types, x = plot.vtk_mesh(V)
grid = pyvista.UnstructuredGrid(cells, types, x)
grid.point_data["u"] = uh.x.array.real
grid.set_active_scalars("u")
plotter = pyvista.Plotter()
plotter.add_mesh(grid, show_edges=True)
warped = grid.warp_by_scalar()
plotter.add_mesh(warped)
if pyvista.OFF_SCREEN:
plotter.screenshot(out_folder / "uh_biharmonic.png")
else:
plotter.show()
except ModuleNotFoundError:
print("'pyvista' is required to visualise the solution")
print("Install 'pyvista' with pip: 'python3 -m pip install pyvista'")
References
Ivo Babuška and Miloš Zlámal. Nonconforming elements in the finite element method with penalty. SIAM Journal on Numerical Analysis, 10(5):863–875, 1973. URL: http://www.jstor.org/stable/2156320 (visited on 2026-02-25).
Philipp Bringmann, Carsten Carstensen, and Julian Streitberger. Local parameter selection in the c0 interior penalty method for the biharmonic equation. Journal of Numerical Mathematics, 32(3):257–273, 2024. doi:10.1515/jnma-2023-0028.
P.G. CIARLET and P.A. RAVIART. A mixed finite element method for the biharmonic equation. In Carl de Boor, editor, Mathematical Aspects of Finite Elements in Partial Differential Equations, pages 125–145. Academic Press, 1974. doi:10.1016/B978-0-12-208350-1.50009-1.
Martin J. Gander and Yongxiang Liu. On the definition of dirichlet and neumann conditions for the biharmonic equation and its impact on associated schwarz methods. In Chang-Ock Lee, Xiao-Chuan Cai, David E. Keyes, Hyea Hyun Kim, Axel Klawonn, Eun-Jae Park, and Olof B. Widlund, editors, Domain Decomposition Methods in Science and Engineering XXIII, 303–311. Cham, 2017. Springer International Publishing. doi:10.1007/978-3-319-52389-7_31.
Emmanuil H. Georgoulis and Paul Houston. Discontinuous galerkin methods for the biharmonic problem. IMA Journal of Numerical Analysis, 29(3):573–594, 2009. doi:10.1093/imanum/drn015.