Solve the Poisson equation on the curved domain

Hello, everyone!Currently, I want to conduct numerical simulation for the curved domain. Since I’m not very familiar with it, I started with the simplest Poisson equation. Below is the code I used to calculate it with fenicsx.

import gmsh    
from dolfinx.io import gmshio
from dolfinx.fem.petsc import LinearProblem
from mpi4py import MPI
import numpy as np
from dolfinx import fem
import ufl
from dolfinx import default_scalar_type
import numpy as np
def solve_poisson_on_mesh(h, element_order, FE_order):
    gmsh.initialize()

    membrane = gmsh.model.occ.addDisk(0, 0, 0, 1, 1)
    gmsh.model.occ.synchronize()
    gdim = 2
    gmsh.model.addPhysicalGroup(gdim, [membrane], 1)
    gmsh.option.setNumber("Mesh.CharacteristicLengthMin", h)
    gmsh.option.setNumber("Mesh.CharacteristicLengthMax", h)
    gmsh.option.setNumber("Mesh.ElementOrder",  element_order)
    gmsh.option.setNumber("Mesh.SecondOrderLinear", 0)  
    gmsh.option.setNumber("Mesh.SecondOrderIncomplete", 0)  
    gmsh.model.mesh.generate(gdim)

    gmsh_model_rank = 0
    mesh_comm = MPI.COMM_WORLD
    domain, cell_markers, facet_markers = gmshio.model_to_mesh(gmsh.model, mesh_comm, gmsh_model_rank, gdim=gdim)

    print(f"网格几何度数: {domain.geometry.cmap.degree}")
    print(f"几何映射类型: {domain.geometry.cmap.__class__.__name__}")

    print(f"拓扑维度: {domain.topology.dim}")
    print(f"顶点数量: {domain.geometry.x.shape[0]}")
    print(f"单元数量: {domain.topology.index_map(domain.topology.dim).size_global}")

    boundary_points = domain.geometry.x
    radii = np.sqrt(boundary_points[:, 0]**2 + boundary_points[:, 1]**2)
    on_boundary = np.isclose(radii, 1.0, atol=0.02)  
    print(f"总点数: {len(boundary_points)}")
    print(f"边界点数量: {np.sum(on_boundary)}")
    if np.sum(on_boundary) > 0:
       print(f"边界点半径范围: [{radii[on_boundary].min():.6f}, {radii[on_boundary].max():.6f}]")


    def exact_solution(x):
        """精确解:w = (1 - x² - y²) * sin(πx) * cos(πy)"""
        r2 = x[0]**2 + x[1]**2
        return (1 - r2) * np.sin(np.pi*x[0]) * np.cos(np.pi*x[1])
    
    def source_term(x):
        """手动计算的源项:f = -∇²w"""
        x_val = x[0]
        y_val = x[1]
        r2 = x_val**2 + y_val**2
        
        term1 = (4 + 2*np.pi**2 * (1 - r2)) * np.sin(np.pi*x_val) * np.cos(np.pi*y_val)
        term2 = 4*np.pi*x_val * np.cos(np.pi*x_val) * np.cos(np.pi*y_val)
        term3 = -4*np.pi*y_val * np.sin(np.pi*x_val) * np.sin(np.pi*y_val)
        
        return term1 + term2 + term3

    def on_boundary(x):
        return np.isclose(np.sqrt(x[0]**2 + x[1]**2), 1)

    x = ufl.SpatialCoordinate(domain)
    V = fem.functionspace(domain, ("Lagrange", FE_order))
   # f = fem.Constant(domain, default_scalar_type(4))
    f = fem.Function(V)
    f.interpolate(source_term)


    boundary_dofs = fem.locate_dofs_geometrical(V, on_boundary)
    bc = fem.dirichletbc(default_scalar_type(0), boundary_dofs, V)
    u = ufl.TrialFunction(V)
    v = ufl.TestFunction(V)
    a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx
    L = f * v * ufl.dx
    problem = LinearProblem(a, L, bcs=[bc], petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
    uh = problem.solve()

    V_exact = fem.functionspace(domain, ("Lagrange", FE_order +2))  
    u_exact_func = fem.Function(V_exact)
    u_exact_func.interpolate(exact_solution)

    error_L2 = fem.form(ufl.inner(uh - u_exact_func, uh - u_exact_func) * ufl.dx)
    L2_error = np.sqrt(fem.assemble_scalar(error_L2))

    error_H1 = fem.form(ufl.inner(uh - u_exact_func, uh - u_exact_func) * ufl.dx + 
                   ufl.inner(ufl.grad(uh - u_exact_func), ufl.grad(uh - u_exact_func)) * ufl.dx)
    H1_error = np.sqrt(fem.assemble_scalar(error_H1))
    gmsh.finalize()
    return L2_error, H1_error,h

def main():
    mesh_sizes = [0.2, 0.1, 0.05, 0.025, 0.0125, 0.00625]
    
    L2_errors = []
    H1_errors = []
    h_list = []
    
    print("开始收敛性分析...")
    print("=" * 60)
    
    for i, h in enumerate(mesh_sizes):
        print(f"\n计算网格尺寸 h = {h}")
        L2_error, H1_error, actual_h = solve_poisson_on_mesh(h, 2, 2)
        
        L2_errors.append(L2_error)
        H1_errors.append(H1_error)
        h_list.append(actual_h)
        
        print(f"  网格尺寸: {h:.3f}")
        print(f"  L2误差: {L2_error:.6e}")
        print(f"  H1误差: {H1_error:.6e}")
    
    
    print("\n" + "=" * 60)
    print("收敛阶分析:")
    print("=" * 60)
    print("网格尺寸     L2误差        H1误差        L2收敛阶    H1收敛阶")
    print("-" * 75)
    
    for i in range(len(mesh_sizes)):
        if i == 0:
            print(f"{h_list[i]:.3f}      {L2_errors[i]:.2e}    {H1_errors[i]:.2e}      -         -")
        else:
            # 计算收敛阶
            rate_L2 = np.log(L2_errors[i-1] / L2_errors[i]) / np.log(h_list[i-1] / h_list[i])
            rate_H1 = np.log(H1_errors[i-1] / H1_errors[i]) / np.log(h_list[i-1] / h_list[i])
            
            print(f"{h_list[i]:.3f}      {L2_errors[i]:.2e}    {H1_errors[i]:.2e}      {rate_L2:.3f}       {rate_H1:.3f}")
if __name__ == "__main__":
    print("程序开始执行...")
    main()
    print("程序执行完成")

The result is:

============================================================
Convergence order analysis:
============================================================
h             L2 error        H1error     L2 convergence order    H1 convergence order
---------------------------------------------------------------------------
0.200      2.23e-03    8.57e-02      -         -
0.100      2.98e-04    2.32e-02      2.908       1.886
0.050      3.74e-05    5.89e-03      2.992       1.976
0.025      4.70e-06    1.48e-03      2.994       1.994
0.013      5.91e-07    3.72e-04      2.990       1.992
0.006      7.41e-08    9.31e-05      2.997       1.998

What I don’t quite understand is whether the calculation in the curved region differs from that in the straight region, and this difference lies in the import process of GMSH? Are there no differences in the others? Is this result of my operation reasonable? Is the degree of approximation controlled by the line gmsh.option.setNumber("Mesh.ElementOrder", element_order)? Looking forward to your reply!

The difference lies in the imported mesh (which then will cascade the changes down to the assembled forms).
Running with (mesh degree 2, element_degree 2), you get:

0.200      2.23e-03    8.57e-02      -         -
0.100      2.98e-04    2.32e-02      2.908       1.886
0.050      3.74e-05    5.89e-03      2.992       1.976
0.025      4.70e-06    1.48e-03      2.994       1.994
0.013      5.91e-07    3.72e-04      2.990       1.992
0.006      7.41e-08    9.31e-05      2.997       1.998

Running with element degree 2 and mesh degree 1 yields:

0.200      5.89e-02    6.11e-01      -         -
0.100      3.12e-02    4.53e-01      0.914       0.434
0.050      1.54e-02    3.21e-01      1.023       0.496
0.025      7.71e-03    2.29e-01      0.996       0.489
0.013      3.88e-03    1.63e-01      0.989       0.490
0.006      3.16e-06    2.11e-04      10.261       9.589

Running with 1 and 1 yields:

0.200      6.82e-02    7.52e-01      -         -
0.100      1.90e-02    3.84e-01      1.842       0.967
0.050      4.86e-03    1.92e-01      1.971       1.000
0.025      1.22e-03    9.62e-02      1.994       0.999
0.013      3.06e-04    4.82e-02      1.992       0.996
0.006      7.67e-05    2.41e-02      1.999       0.999

As you can see it clearly has an effect.

DOLFINx doesnt rely on iso-parameteric discretizations, the mesh can be represented by a different degree element than the unknown.

The calculation doesn’t differ from a finite element kernel perspective, the same kernel (with a second order element representing the jacobian) is used in all cells. It is just that the cells within the mesh that are straight ends up with a constant jacobian.
Some of this is discussed in:

and the previous paragraphs.

Thank you for your reply! The meaning is that, according to the principle of Isoparametric element, the grid degree must be consistent with the degree of the element in order to ensure the correctness of the convergence order. The key point is to correctly import the curved mesh? Is the code for assembling and solving the same as the code in the straight-edge area?

You can pick an isoparametric combination, sub parametric or super parametric combination for the (mesh geometry, unknown space) pair. Depending on what you pick, DOLFINx will select the correct kernel, quadrature rule etc for what you have specified.
As you saw by my simple experiment above, you see you get different rates depending on what parameters you select.