Convergence issue in Periodic BCs for Poisson eq in 1D

Hi everyone,

I am implementing a FEM framework in C++ and am using dolfinx to have a prototype that I can check against.
I am currently trying to implement periodic boundary conditions for the Poisson equation, with order 1 Lagrange elements in 1D and a 5th order quadrature (satisfying > 2p+1).

To study the convergence, I am setting the right hand side \pi^2 sin(\pi x) and solving in the domain [-1, 1]. This gives correct 2nd order convergence.
However, when I change the right hand side to \pi^2 cos(\pi x), it converges with a lower rate (order 1).

Does this mean there is something wrong in my implementation of the boundary conditions? I have printed out and checked a lot of values and it seems correct, and it does converge, just with a lower order. Or am I missing some mathematical nuance that prescribes this lower order of convergence for this problem?

I have also tried shifting the domain to [0,2] instead of [-1,1] with the right hand side as \pi^2 sin(\pi x) and I also get a lower order of convergence.

Here is the code I am using for the sin case (and I just change the exact solution and the right hand side to cos when checking that):

from dolfinx import fem, mesh, default_scalar_type, io
from mpi4py import MPI
import ufl
import numpy as np
from dolfinx_mpc import LinearProblem, MultiPointConstraint
from petsc4py import PETSc

comm = MPI.COMM_WORLD
rank = comm.Get_rank()
tol = 250 * np.finfo(default_scalar_type).resolution

def run(num_els=10,
        space_order=1,
        interpolate_rhs=False,
        quadrature_degree=5,
        write_vtk=False):

    exact_sol = lambda x : np.sin(np.pi * (x[0]))
    domain = mesh.create_interval(comm, num_els, [-1.0, 1.0])
    dim = domain.topology.dim #dim=1
    fdim = dim - 1# facet dimension
    V = fem.functionspace(domain, ("Lagrange", space_order),)
    u_exact = fem.Function(V, name="u_exact")
    u_exact.interpolate( exact_sol )

    def periodic_boundary(x):
        return np.isclose(x[0], 1, atol=tol)

    def periodic_relation(x):
        out_x = np.zeros_like(x)
        out_x[0] = -x[0]
        out_x[1] = +x[1]
        out_x[2] = +x[2]
        return out_x

    bcs = []

    mpc = MultiPointConstraint(V)
    mpc.create_periodic_constraint_geometrical(V, periodic_boundary, periodic_relation, bcs=None)
    mpc.finalize()

    (u, v) = (ufl.TrialFunction(V), ufl.TestFunction(V))
    dx = ufl.dx(metadata={"quadrature_degree":quadrature_degree})
    a_ufl = ufl.dot( ufl.grad(u), ufl.grad(v) ) * dx

    x = ufl.SpatialCoordinate(domain)
    if interpolate_rhs:
        f = fem.Function(V, name="f_interpolated")
        f.interpolate(lambda x : (np.pi**2)*exact_sol(x))
    else:
        f = ufl.pi**2 * ufl.sin(ufl.pi * (x[0]))
    l_ufl = v * f * dx

    # Solve
    problem = LinearProblem(a_ufl, l_ufl, mpc, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
    solver = problem.solver
    solver_prefix = "dolfinx_mpc_solve_{}".format(id(solver))
    solver.setOptionsPrefix(solver_prefix)
    petsc_options = {
        "ksp_type": "cg",
        "ksp_rtol": 1e-6,
        "pc_type": "hypre",
        "pc_hypre_type": "boomeramg",
        "pc_hypre_boomeramg_max_iter": 1,
        "pc_hypre_boomeramg_cycle_type": "v",  # ,
        # "pc_hypre_boomeramg_print_statistics": 1
    }
    # Set PETSc options
    opts = PETSc.Options()  # type: ignore
    opts.prefixPush(solver_prefix)
    if petsc_options is not None:
        for k, v in petsc_options.items():
            opts[k] = v
    opts.prefixPop()
    solver.setFromOptions()
    nullspace = PETSc.NullSpace().create(constant=True,comm=MPI.COMM_WORLD)
    problem.A.setNullSpace(nullspace)
    uh = problem.solve()
    uh.name = "uh"

    # Compute error
    uex = ufl.sin(ufl.pi * (x[0])) # exact sol
    L2_error = fem.form(ufl.inner(uh - uex, uh - uex) * dx)
    error_local = fem.assemble_scalar(L2_error) # local to rank
    error_L2 = np.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))

    error_max = np.max(np.abs(u_exact.x.array-uh.x.array))
# Only print the error on one process
    if domain.comm.rank == 0:
        print(f"Ran with {num_els} elements, interpolate_rhs = {interpolate_rhs}, quadrature_degree = {quadrature_degree}, order {space_order} elements.")
        print(f"L2 error : {error_L2:.2e}")
        print(f"Max nodal error : {error_max:.2e}")
        print("===========================")
    if write_vtk:
        with io.VTKFile(domain.comm, f"post/solution{num_els}_els.pvd", "w") as pf:
            pf.write_function([uh,u_exact])
    return error_L2, error_max

def get_convergence_rate():
    space_order = 1
    interpolate_rhs = True
    l2_errors = []
    quadrature_degree = 5
    total_elements = np.array([4, 8, 16, 32, 64])
    l2_errors = []
    for num_els in total_elements:
        l2_error, _ = run(num_els=num_els,
                            space_order=space_order,
                            interpolate_rhs=interpolate_rhs,
                            quadrature_degree=quadrature_degree,
                            write_vtk=True)
        l2_errors.append( l2_error )
    domain_size = 2.0
    element_sizes = domain_size / total_elements
    line = np.polyfit(np.log(element_sizes), np.log(l2_errors), 1) # fit degree 1 polynomial to the logs
    convergence_rate = line[0]
    if rank==0:
        print(f"Ran with interpolate_rhs = {interpolate_rhs}, quadrature_degree = {quadrature_degree}, order {space_order} elements.")
        print(f"Convergence rate = {convergence_rate}")

if __name__=="__main__":
    get_convergence_rate()

Thanks in advance for any help!

1 Like

Looking closely at this example, I’ve found a bug with DOLFINx_MPC: Zero out backsubstitution by jorgensd · Pull Request #155 · jorgensd/dolfinx_mpc · GitHub
I will backport this to v0.9.0.
Here is a minimal example from my end:

from dolfinx import fem, mesh, default_scalar_type, io
from mpi4py import MPI
import ufl
import numpy as np
from dolfinx_mpc import LinearProblem, MultiPointConstraint
from petsc4py import PETSc

comm = MPI.COMM_WORLD
rank = comm.Get_rank()
tol = 250 * np.finfo(default_scalar_type).resolution

def run(num_els=10,
        space_order=1,
        interpolate_rhs=False,
        quadrature_degree=5,
        write_vtk=False):

    exact_sol = lambda x : np.cos(np.pi * (x[0]))
    domain = mesh.create_interval(comm, num_els, [-1.0, 1.0])
    dim = domain.topology.dim #dim=1
    fdim = dim - 1# facet dimension
    V = fem.functionspace(domain, ("Lagrange", space_order),)

    def periodic_boundary(x):
        return np.isclose(x[0], 1, atol=tol)

    def periodic_relation(x):
        out_x = np.zeros_like(x)
        out_x[0] = -x[0]
        out_x[1] = +x[1]
        out_x[2] = +x[2]
        return out_x

    bcs = []

    mpc = MultiPointConstraint(V)
    mpc.create_periodic_constraint_geometrical(V, periodic_boundary, periodic_relation, bcs=None)
    mpc.finalize()
    (u, v) = (ufl.TrialFunction(V), ufl.TestFunction(V))
    dx = ufl.dx(metadata={"quadrature_degree":quadrature_degree}, domain=domain)
    a_ufl = ufl.dot( ufl.grad(u), ufl.grad(v) ) * dx

    x = ufl.SpatialCoordinate(domain)
    if interpolate_rhs:
        f = fem.Function(mpc.function_space, name="f_interpolated")
        f.interpolate(lambda x : (np.pi**2)*exact_sol(x))
    else:
        f = ufl.pi**2 * ufl.cos(ufl.pi * (x[0]))
    l_ufl = v * f * dx

    # Solve
    problem = LinearProblem(a_ufl, l_ufl, mpc, bcs=bcs, petsc_options={"ksp_type": "preonly", "pc_type": "lu",
                                                                       "pc_factor_mat_solver_type": "mumps"})
    solver = problem.solver
    nullspace = PETSc.NullSpace().create(constant=True,comm=MPI.COMM_WORLD)
    problem.A.setNullSpace(nullspace)
    uh = problem.solve()
    assert(solver.getConvergedReason() > 0)
    uh.name = "uh"

    uh_avg = domain.comm.allreduce(fem.assemble_scalar(fem.form(uh*dx)), op = MPI.SUM)
    vol = domain.comm.allreduce(fem.assemble_scalar(fem.form(1*dx)), op = MPI.SUM)

    uh.x.array[:] -= uh_avg/vol
    print(uh_avg)
    import dolfinx
    with dolfinx.io.XDMFFile(comm, f"post/solution{num_els}_els.xdmf", "w") as xdmf:
        xdmf.write_mesh(domain)
        xdmf.write_function(uh)
    # Compute error


    uex = ufl.cos(ufl.pi * (x[0])) # exact sol
    L2_error = fem.form(ufl.inner(uh - uex, uh - uex) * dx)
    error_local = fem.assemble_scalar(L2_error) # local to rank
    error_L2 = np.sqrt(domain.comm.allreduce(error_local, op=MPI.SUM))
    u_exact = fem.Function(mpc.function_space, name="u_exact")
    u_exact.interpolate( exact_sol )
    error_max = np.max(np.abs(u_exact.x.array-uh.x.array))
    # Only print the error on one process
    if domain.comm.rank == 0:
        print(f"Ran with {num_els} elements, interpolate_rhs = {interpolate_rhs}, quadrature_degree = {quadrature_degree}, order {space_order} elements.")
        print(f"L2 error : {error_L2:.2e}")
        print(f"Max nodal error : {error_max:.2e}")
        print("===========================")
    if write_vtk:
        with io.VTKFile(domain.comm, f"post/solution{num_els}_els.pvd", "w") as pf:
            pf.write_function([uh,u_exact])
    return error_L2, error_max

def get_convergence_rate():
    space_order = 1
    interpolate_rhs = False
    l2_errors = []
    quadrature_degree = 5
    total_elements = np.array([4, 8, 16, 32, 64,128])
    l2_errors = []
    for num_els in total_elements:
        l2_error, _ = run(num_els=num_els,
                            space_order=space_order,
                            interpolate_rhs=interpolate_rhs,
                            quadrature_degree=quadrature_degree,
                            write_vtk=True)
        l2_errors.append( l2_error )
    l2_errors = np.array(l2_errors)
    domain_size = 2.0
    element_sizes = np.array(domain_size / total_elements)
    convergence_rates = np.log(l2_errors[1:] / l2_errors[:-1]) / np.log(element_sizes[1:] / element_sizes[:-1])

    if rank==0:
        print(f"Ran with interpolate_rhs = {interpolate_rhs}, l2_errorquadrature_degree = {quadrature_degree}, order {space_order} elements.")
        print(f"Convergence rate = {convergence_rates}")

if __name__=="__main__":
    get_convergence_rate()

yielding

0.9899494936611666
Ran with 4 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 2.14e-01
Max nodal error : 4.20e-05
===========================
-0.6206430317337321
Ran with 8 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 5.56e-02
Max nodal error : 6.00e-07
===========================
-0.08661609640610121
Ran with 16 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 1.40e-02
Max nodal error : 9.16e-09
===========================
-0.12925354135215822
Ran with 32 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 3.52e-03
Max nodal error : 1.42e-10
===========================
-0.005364926055996229
Ran with 64 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 8.80e-04
Max nodal error : 2.22e-12
===========================
-0.0015559588202778002
Ran with 128 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 2.20e-04
Max nodal error : 2.16e-14
===========================
Ran with interpolate_rhs = False, l2_errorquadrature_degree = 5, order 1 elements.
Convergence rate = [1.94233737 1.98562101 1.99640722 1.99910192 1.99977549]

for cos
and

0.024280886260403152
Ran with 4 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 2.14e-01
Max nodal error : 4.20e-05
===========================
-0.32380141271071217
Ran with 8 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 5.56e-02
Max nodal error : 6.00e-07
===========================
-0.3176520858276729
Ran with 16 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 1.40e-02
Max nodal error : 9.16e-09
===========================
-0.2243103313709249
Ran with 32 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 3.52e-03
Max nodal error : 1.42e-10
===========================
-0.00572317288071495
Ran with 64 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 8.80e-04
Max nodal error : 2.22e-12
===========================
-0.0007009677416288028
Ran with 128 elements, interpolate_rhs = False, quadrature_degree = 5, order 1 elements.
L2 error : 2.20e-04
Max nodal error : 1.85e-14
===========================
Ran with interpolate_rhs = False, l2_errorquadrature_degree = 5, order 1 elements.
Convergence rate = [1.94233737 1.98562101 1.99640722 1.99910192 1.99977549]

for sin.

Note that to get the correct convergence rates, you have to average the function after solving to zero (and with the fix from my PR). This is because since you have a constant null space, it is solved with an arbitrary constant added.

2 Likes

Thank you very much for your help! Thanks for the working example and the note about averaging to zero - that is definitely a mistake I was making.

Confused with the periodic_relation? Say if we have a periodic interval [a, b] with

def periodic_boundary(x):
    return np.isclose(x[0], b, atol=tol)

then how to define out_x[0] = ?

def periodic_relation(x):
    out_x = x.copy()
    out_x[0] = a
    return out_x

The idea is that you map the coordinates of the periodic boundary (in your case (b, y, z)) to the corresponding coordinate on the other boundary, i.e. (a, y, z)