Adaptive eigenvalue problem on an L-shaped domain

This demo computes the lowest eigenvalue of the Dirichlet Laplacian on an L-shaped domain to within a guaranteed error tolerance using adaptive mesh refinement. The error estimate in the eigenvalue is based on solving the problem with two complementary discretisations: a conforming method naturally yields an upper bound, while the Crouzeix-Raviart non-conforming method yields a lower bound after postprocessing proposed by Carstensen & Gedicke. The demo also demonstrates that adaptive mesh refinement is strictly more efficient than uniform refinement, due to the singularity of the eigenfunction at the re-entrant corner.

The demo was contributed by Patrick Farrell.

We consider the Dirichlet eigenvalue problem for the Poisson equation on an L-shaped domain \(\Omega\):

\[\begin{split}-\Delta u &= \lambda u \quad \text{in } \Omega, \\ u &= 0 \quad \text{on } \partial \Omega.\end{split}\]

We aim to compute the lowest eigenvalue. Since the domain has a re-entrant corner, the eigenfunction has a singularity. Thus, uniform mesh refinement leads to suboptimal convergence. We demonstrate an adaptive strategy driven by a residual-based a posteriori error estimator. To establish rigorous bounds on the true eigenvalue \(\lambda\), we use conforming elements (CG) for an upper bound [Bof10], and nonconforming elements (CR) with Carstensen-Gedicke postprocessing [CG14] for a guaranteed lower bound.

We start by importing the necessary libraries:

from firedrake import *
from netgen.occ import *

We then define the L-shaped domain using Netgen’s Open CASCADE technology (OCC) interface, and generate an initial mesh:

rect1 = WorkPlane(Axes((0,0,0), n=Z, h=X)).Rectangle(1,2).Face()
rect2 = WorkPlane(Axes((0,1,0), n=Z, h=X)).Rectangle(2,1).Face()
L = rect1 + rect2

geo = OCCGeometry(L, dim=2)
ngmesh = geo.GenerateMesh(maxh=0.5)
mesh = Mesh(ngmesh)

We create a function to solve the eigenvalue problem for both continuous (CG) and nonconforming Crouzeix-Raviart (CR) [CR73] elements. By the Rayleigh-Ritz principle [Bof10, GW12], the CG solution provides an upper bound \(\lambda_{\text{ub}}\). The CR solution gives a discrete eigenvalue \(\lambda_{\text{CR}}\), which can be postprocessed to yield a guaranteed lower bound:

\[\lambda_{\text{lb}} = \frac{\lambda_{\text{CR}}}{1 + \kappa_{\text{CR}}^2 h_{\max}^2 \lambda_{\text{CR}}}\]

where \(\kappa_{\text{CR}} \approx 0.1893\) is the constant established by Carstensen and Gedicke [CG14]. A general theory for deriving lower bounds for eigenvalues with nonconforming methods has been developed by Hu et al. [HHL14]. We will use the postprocessed lower bound to terminate the adaptive iteration, while for technical reasons we will plot the Galerkin gap \(\lambda_{\text{CG}} - \lambda_{\text{CR}}\) to demonstrate optimal convergence.

To efficiently compute the smallest eigenvalue, we configure SLEPc using a solver parameters dictionary. We specify a Krylov-Schur eigensolver (eps_type) and a shift-and-invert spectral transformation (st_type) with a target of zero (eps_target). We also flag the generalized eigenvalue problem as Hermitian (eps_gen_hermitian) and request the smallest real eigenvalue (eps_smallest_real).

def solve_eigenproblem(mesh):
    h_symbolic = CellDiameter(mesh)
    DG0 = FunctionSpace(mesh, "DG", 0)
    h_cell = Function(DG0).interpolate(h_symbolic)
    with h_cell.dat.vec_ro as hvec:
        h_max = hvec.max()[1]
    eigenfunction = None

    for space in ["CG", "CR"]:
        V = FunctionSpace(mesh, space, 1)

        u = TrialFunction(V)
        v = TestFunction(V)

        a = inner(grad(u), grad(v))*dx
        b = inner(u, v)*dx
        bc = DirichletBC(V, 0, "on_boundary")
        eigenproblem = LinearEigenproblem(a, b, bc)

        sp = {
              "eps_gen_hermitian": None,
              "eps_smallest_real": None,
              "eps_monitor_cancel": None,
              "eps_type": "krylovschur",
              "eps_target": 0,
              "st_type": "sinvert",
              }

        eigensolver = LinearEigensolver(eigenproblem, 1, solver_parameters=sp)
        eigensolver.solve()

        if space == "CR":
            lambda_CR = eigensolver.eigenvalue(0).real
            kappa_CR = 0.1893
            lambda_lb = lambda_CR / (1 + kappa_CR**2 * h_max**2 * lambda_CR)
        if space == "CG":
            lambda_ub = eigensolver.eigenvalue(0).real
            eigenfunction = eigensolver.eigenfunction(0)[0]

    eigenfunction.rename("Eigenfunction")
    return (lambda_lb, lambda_ub, lambda_CR, eigenfunction)

These bounds do not describe where the mesh should be refined so as to reduce the error. For this purpose we employ a standard residual-based a posteriori error estimator [DPR03, Lar00]. Note that this assumes there is a single eigenfunction associated with the lowest eigenvalue; if the eigenvalue were of higher multiplicity the estimator would need to consider the entire eigenspace [BG14].

def estimate_error(mesh, uh, lam):
    W = FunctionSpace(mesh, "DG", 0)
    eta_sq = Function(W)
    w = TestFunction(W)
    h = CellDiameter(mesh)
    n = FacetNormal(mesh)
    v = CellVolume(mesh)

    G = (
          inner(eta_sq / v, w)*dx
        - inner(h**2 * (lam*uh + div(grad(uh)))**2, w) * dx
        - inner(h('+')/2 * jump(grad(uh), n)**2, w('+')) * dS
        - inner(h('-')/2 * jump(grad(uh), n)**2, w('-')) * dS
        )

    sp = {"mat_type": "matfree", "ksp_type": "preonly", "pc_type": "jacobi"}
    solve(G == 0, eta_sq, solver_parameters=sp)
    eta = Function(W).interpolate(sqrt(eta_sq))

    with eta.dat.vec_ro as eta_:
        error_est = eta_.norm()
    return (eta, error_est)

We define a function to adapt the mesh by refining elements with large error indicators, using the maximum Dörfler-like marking strategy with \(\theta = 0.5\):

def adapt(mesh, eta):
    W = FunctionSpace(mesh, "DG", 0)
    markers = Function(W)

    with eta.dat.vec_ro as eta_:
        eta_max = eta_.max()[1]

    theta = 0.5
    should_refine = conditional(gt(eta, theta*eta_max), 1, 0)
    markers.interpolate(should_refine)

    return mesh.refine_marked_elements(markers)

Finally, we run the adaptive loop until the upper and lower bounds agree to within a tolerance.

max_iterations = 20
error_estimators = []
dofs = []
err = 1

for i in range(max_iterations):
    lam_lb, lam_ub, lam_CR, uh = solve_eigenproblem(mesh)
    err = lam_ub - lam_lb
    gap = lam_ub - lam_CR
    error_estimators.append(gap)
    dofs.append(uh.function_space().dim())
    print(f"Level {i}: Upper bound {lam_ub:.5f}, Lower bound {lam_lb:.5f}, Bound gap {err:.5e}, Galerkin gap {gap:.5e}")

    VTKFile(f"l_eigenfunction_{i}.pvd").write(uh)

    if err < 1e-2 or dofs[-1] > 1000000:
        break

    eta, _ = estimate_error(mesh, uh, lam_ub)
    mesh = adapt(mesh, eta)

To demonstrate that adaptivity is necessary to achieve the optimal convergence rate, we can run the same script with \(\theta = 0\), which forces uniform refinement (all cells are marked for refinement at every step). We make this optional by guarding it behind the Boolean run_uniform.

run_uniform = True

if run_uniform:
    mesh_uniform = Mesh(ngmesh)
    uniform_error_estimators = []
    uniform_dofs = []

    def adapt_uniform(mesh, eta):
        markers = Function(FunctionSpace(mesh, "DG", 0)).assign(1.0)
        return mesh.refine_marked_elements(markers)

    for i in range(max_iterations):
        lam_lb, lam_ub, lam_CR, uh = solve_eigenproblem(mesh_uniform)
        err = lam_ub - lam_lb
        gap = lam_ub - lam_CR
        uniform_error_estimators.append(gap)
        uniform_dofs.append(uh.function_space().dim())
        if err < 5e-3 or uniform_dofs[-1] > 1000000:
            break
        eta, _ = estimate_error(mesh_uniform, uh, lam_ub)
        mesh_uniform = adapt_uniform(mesh_uniform, eta)

We can plot the convergence of the Galerkin gap \(\lambda_{\text{ub}} - \lambda_{\text{CR}}\) against the number of degrees of freedom. With adaptivity, we achieve the optimal \(O(N^{-1})\) convergence rate. For uniform refinement, the error is initially dominated by the smooth part of the solution (yielding a pre-asymptotic \(O(N^{-1})\) rate), but as the mesh is refined, the singularity inevitably dominates and limits the asymptotic convergence to the suboptimal rate of \(O(N^{-2/3})\).

try:
    import matplotlib.pyplot as plt
    import numpy as np

    plt.grid()
    plt.loglog(dofs, error_estimators, '-ok', label=r"Adaptive refinement ($\theta = 0.5$)")
    scaling = error_estimators[0] / dofs[0]**-1
    plt.loglog(dofs, np.array(dofs)**(-1.0) * scaling, '--', label="Optimal convergence $N^{-1}$")

    if run_uniform:
        plt.loglog(uniform_dofs, uniform_error_estimators, '-or', label=r"Uniform refinement ($\theta = 0$)")
        scaling_uniform = uniform_error_estimators[-1] / uniform_dofs[-1]**(-2.0/3.0)
        plt.loglog(uniform_dofs, np.array(uniform_dofs)**(-2.0/3.0) * scaling_uniform, ':r', label=r"Suboptimal convergence $N^{-2/3}$")
    plt.xlabel("Number of degrees of freedom $N$")
    plt.ylabel(r"Galerkin gap $\lambda_{\text{ub}} - \lambda_{\text{CR}}$")
    plt.legend()
    plt.savefig("adaptive_eigenvalue_convergence.png")
except ImportError:
    warning("Matplotlib not imported")
../_images/adaptive_eigenvalue_convergence.png

Convergence of the Galerkin gap \(\lambda_{\text{ub}} - \lambda_{\text{CR}}\). Note that the adaptive scheme achieves the optimal convergence rate of \(O(N^{-1})\), whereas uniform refinement is limited to the suboptimal rate of \(O(N^{-2/3})\).

To visualize how the adaptive algorithm resolves the singularity, the sequence of images below shows the mesh (at refinement level 15), zooming into the re-entrant corner at 10x, 100x, and 1000x magnification.

../_images/mesh_zoom_10x.png ../_images/mesh_zoom_100x.png ../_images/mesh_zoom_1000x.png

References

[Bof10] (1,2)

Daniele Boffi. Finite element approximation of eigenvalue problems. Acta Numerica, 19:1–120, 2010. doi:10.1017/S0962492910000012.

[BG14]

Daniele Boffi and Lucia Gastaldi. Some remarks on finite element approximation of multiple eigenvalues. Applied Numerical Mathematics, 79:18–28, 2014. doi:10.1016/j.apnum.2012.08.006.

[CG14] (1,2)

Carsten Carstensen and Joscha Gedicke. Guaranteed lower bounds for eigenvalues. Mathematics of Computation, 83(290):2605–2629, 2014. doi:10.1090/S0025-5718-2014-02833-0.

[CR73]

Michel Crouzeix and Pierre-Arnaud Raviart. Conforming and nonconforming finite element methods for solving the stationary Stokes equations I. Revue française d'automatique informatique recherche opérationnelle, 7(R3):33–75, 1973. doi:10.1051/m2an/197307r300331.

[DPR03]

Ricardo G. Durán, Claudio Padra, and Rodolfo Rodríguez. A posteriori error estimates for the finite element approximation of eigenvalue problems. Mathematical Models and Methods in Applied Sciences, 13(8):1219–1229, 2003. doi:10.1142/S0218202503002878.

[GW12]

Martin J. Gander and Gerhard Wanner. From Euler, Ritz, and Galerkin to modern computing. SIAM Review, 54(4):627–666, 2012. doi:10.1137/100804036.

[HHL14]

Jun Hu, Yunqing Huang, and Qun Lin. Lower bounds for eigenvalues of elliptic operators: by nonconforming finite element methods. Journal of Scientific Computing, 61(1):196–221, 2014. doi:10.1007/s10915-014-9821-5.

[Lar00]

Mats G. Larson. A posteriori and a priori error analysis for finite element approximations of self-adjoint elliptic eigenvalue problems. SIAM Journal on Numerical Analysis, 38(2):608–625, 2000. doi:10.1137/S0036142997320164.