Order of convergence largely oscillates

I am using GMSH to produce a sequence of meshes of an annulus (centered at 0, inner radius 1, outer radius 2), with greater resolution on the inner circle to get better accuracy:
Schermata da 2022-07-09 18-11-34

I am solving a poisson equation with exact solution u_ex = 0 if x[0]<=0, x[0]**2 otherwise, Dirichlet BCs on the inner ring, Neumann on the outer ring.

Computing the achieved order of convergence (L2 norm) yields: [ 3.04670631 -0.08825267 2.56832882 2.30941509 1.64144749 0.59804678 2.70940579], which oscillates a lot (the solution is H2 regular). A similar behaviour is obtained with Dirichlet BCs everywhere.

What could be the explanation for this? Is it a problem of the imposition of the boundary conditions?

Thank you in advance…

Let me post the relevant code, mesh generation comes below:



# %% Some data (e.g. exact solution and what not)

# Mesh path
mesh_path = "/home/annulus/"

# Resolutions
resolutions = [0.2, .1, .05, .025, .0125, 0.00625, 0.003125] # mesh width array


# Exact solution and corresponding source
class solution(UserExpression):
    def __init__(self, **kwargs):
        """ Construct the source function """
        super().__init__(self, **kwargs)

    def eval(self, values, x):
        """ Evaluate the source function """
        if x[0] >= 0:
            values[0] = -x[0] ** 2 / 2
        else:
            values[0] = 0


class source(UserExpression):
    def __init__(self, **kwargs):
        """ Construct the source function """
        super().__init__(self, **kwargs)

    def eval(self, values, x):
        """ Evaluate the source function """
        if x[0] >= 0:
            values[0] = 1
        else:
            values[0] = 0


class normal_derivative(UserExpression):
    def __init__(self, **kwargs):
        super().__init__(self, **kwargs)

    def eval(self, values, x):
        if x[0] >= 0:
            if x[0] ** 2 + x[1] ** 2 < 1.5 ** 2:
                values[0] = + x[0]**2
            else:
                values[0] = - x[0]**2 / 2
        else:
            values[0] = 0


u_ex = solution()
f = source()
f_D = solution()  # Dirichlet BC
f_N = normal_derivative() # Neumann BC


# %% Read that mesh

def read_mesh(resolution, mesh_path=mesh_path):
    # The volumetric mesh
    mesh = Mesh()
    with XDMFFile(mesh_path + "mesh_" + str(resolution) + ".xdmf") as infile:
        infile.read(mesh)

    # The boundary conditions
    mvc = MeshValueCollection("size_t", mesh, 1) 
    with XDMFFile(mesh_path + "facet_mesh_" + str(resolution) + ".xdmf") as infile:
        infile.read(mvc, "name_to_read")
    mf = MeshFunctionSizet(mesh, mvc)  # remember, tag 3 is inner ring, tag 2 outer ring

    L1 = FiniteElement("Lagrange", mesh.ufl_cell(), 1)
    V = FunctionSpace(mesh, L1)

    return mesh, mf, V


# %% The problem

def solve_single_pb(resolution):

    (mesh, mf, V) = read_mesh(resolution)

    plot(mesh)
    plt.show()

    U = TrialFunction(V)
    v = TestFunction(V)

    # Functional formulation
    dOuterRing = Measure("ds", subdomain_data=mf, subdomain_id=2)

    F = inner(grad(U), grad(v)) * dX - f_N * v * dOuterRing - f * v * dX
    a, L = lhs(F), rhs(F)

    # Boundary conditions
    bcs = [DirichletBC(V, f_D, mf, 3)]  # inner ring, outer ring has already Neumann BCs

    u = Function(V)
    solve(a == L, u, bcs)

    err = errornorm(u_ex, u) 

    return err

# %% Error checking

err_vec = []

for res in resolutions:
    err_vec.append(solve_single_pb(res))

err_vec = np.array(err_vec)
ooc = -np.log2( err_vec[1:]/err_vec[:-1])

print(ooc)

And here, the mesh generation code:


# %%
resolution = .0125/8

# An empty geometry
geometry = pygmsh.geo.Geometry()
# Create a model to add data to
model = geometry.__enter__()

# A circle centered at the origin and radius 1
circle = model.add_circle([0.0, 0.0, 0.0], 2.0, mesh_size=5*resolution)  # meshes are always 3D, I will suppress the third component in case

# A hole
hole = model.add_circle([0.0, 0.0, 0.0], 1.0, mesh_size=1*resolution)

# My surface
plane_surface = model.add_plane_surface(circle.curve_loop,[hole.curve_loop])

# Sinchronize, before adding physical entities
model.synchronize()

#%%

# Tagging/marking boundaries and volume. 
# Boundaries with the same tag should be added simultaneously
model.add_physical([plane_surface], "volume")
model.add_physical(circle.curve_loop.curves, "outer_ring")
model.add_physical(hole.curve_loop.curves, "inner_ring")

# Generate the mesh
geometry.generate_mesh(dim=2)
geometry.save_geometry(path+"annulus_"+str(resolution)+".geo_unrolled")  
gmsh.write(path+"mesh_"+str(resolution)+".msh")
gmsh.clear()
geometry.__exit__()

#%%##########################
# Saving to a better format #
#############################

mesh_from_file = meshio.read(path+"mesh_"+str(resolution)+".msh")

def create_mesh(mesh, cell_type, prune_z = False):
    cells = mesh.get_cells_type(cell_type)  # get the cells of some type: it will change!
    cell_data = mesh.get_cell_data("gmsh:physical", cell_type)
    points = mesh.points[:,:2] if prune_z else mesh.points
    out_mesh = meshio.Mesh(points=points, cells={cell_type: cells}, cell_data={"name_to_read":[cell_data]})
    return out_mesh

# Using the above function, create line and "plane" mesh
line_mesh = create_mesh(mesh_from_file, "line", prune_z=True)
meshio.write(path+"facet_mesh_"+str(resolution)+".xdmf", line_mesh)
triangle_mesh = create_mesh(mesh_from_file, "triangle", prune_z=True)
meshio.write(path+"mesh_"+str(resolution)+".xdmf", triangle_mesh)

Please format your code with markdown syntax, i.e.

```python
def f(x):
    return x
```

Also, without having the complete code, it is very hard to give you any guidance to what is wrong.
There are several things you should consider:

  • Is the source term and/or boundary conditions satisfying the exact equation?
  • what is the magnitude of the actual error?
  • what happens if you use the built in refine function as opposed to reading in new meshes per case?

Thank you for the answer: this is the complete code, mesh generation aside, which I have added in my post.

Let me answer to the bullet points:

  • the source and BCs are built from the exact solution
  • here are some values of the L2 error 8.46565131e-02, 1.02449629e-02, 1.08912340e-02, 1.83625535e-03, 3.70450223e-04, 1.18742156e-04, 7.84467417e-05
  • I am not doing this for a new reasons: the rings are not refined in the way I would like by the built-in refine function (i.e. they don’t get smoother), plus, I don’t know how to “refine the facet indicator”

Have you considered this?

No, since I will be using order 1 finite elements, I should observe order 2 convergence even with a linear approximation of geometry.

After some more tests:

  • the same setup, without hole, has order 2 convergence without oscillations
  • interestingly enough, with respect to my original code, the situation changes to [2.72917419 1.56388078 2.46337273 2.09434059 2.07271966 1.49519038 2.19333781], which looks more promising, if I perform the following modification to the dirichlet BCs:
    def boundary(x):
        return x[0]**2+x[1]**2<1.5**2
    # bcs = [DirichletBC(V, f_D, self.mf, 3)]  # inner ring, outer ring has already Neumann BCs
    bcs = [DirichletBC(V, f_D, boundary)]  # inner ring, outer ring has already Neumann BCs

Do you have any idea why this change is making the situation better? Unfortunately I cannot keep it as is, because I will be doing shape optimization on the inner ring, so, checking the boundary in this way, is no longer doable.

As far as I can tell, you have an issue that you are trying to enforce a discontinuous derivative at the line x=0, which is not aligning with your discrete domain.

If you make sure that the line x=0 is align with the facets of the mesh, I would expect that the convergence of the problem will improve.

I would also suggest to use gmsh to mark the physical groups, as it would avoid adding discretization accuracy to affect the selection of degrees of freedom for the boundary conditions.

Many thanks for your help.

Indeed, removing the jump from the analytical solution, the OOCs are constantly (above) 2.

By the way, I would be curious of your reasons in arguing that a discontinuous second derivative, not aligning with the mesh, would produce this behaviour.

Useless part
However, I wonder why I get two different behaviours when enforcing dirichlet BCs in the two ways:

``` python
def boundary(x):

  • return x[0]*2+x[1]2<1.52
    # bcs = [DirichletBC(V, f_D, self.mf, 3)] # inner ring, outer ring has already Neumann BCs
    bcs = [DirichletBC(V, f_D, boundary)] # inner ring, outer ring has already Neumann BCs
    ```
    *and *

python* *# def boundary(x):* *# return x[0]**2+x[1]**2<1.5**2* *bcs = [DirichletBC(V, f_D, self.mf, 3)] # inner ring, outer ring has already Neumann BCs* *# bcs = [DirichletBC(V, f_D, boundary)] # inner ring, outer ring has already Neumann BCs* *

The two behaviours are:

** first way: 2.72917419 1.56388078 2.46337273 2.09434059 2.07271966 1.49519038 2.19333781*
** original way (with meshfunction): 3.04670631 -0.08825267 2.56832882 2.30941509 1.64144749 0.59804678 2.70940579*

*Is it possible that the mesh function is wrong? *
I have included the code in the original post, for the mesh generation, but I have tripled checked and everything seems okay.

I am also attaching a plot of the following command, for the two ways of imposing Dirichlet boundary conditions:

python* *plot(mesh)* *u = Function(V)* *bcs[0].apply(u.vector())* *pl = plot(u)* *plt.colorbar(pl)* *plt.show()* *

Results:

Schermata da 2022-07-11 15-30-01
Schermata da 2022-07-11 15-30-12

Edit: my mistake here, DirichletBC will modify even interior nodes if instructed to.