Convergence rate of Maxwell simulation using custom L2 norm and cell-center sampling

I am currently implementing Maxwell’s equations with a Cohen Monk perfectly matched layer in a two-dimensional time domain using a rectangular grid. The integration points for error calculation are only taken at the center of each element. Theoretically, the convergence order should be 2, but I consistently obtain an order of 1. I am not sure where the issue lies.Here is my code:

from mpi4py import MPI
from petsc4py import PETSc
import numpy as np
import ufl
import basix

from basix import CellType
from pathlib import Path
from dolfinx import fem, default_real_type, mesh
from dolfinx.fem import (
    Constant,
    Function,
    extract_function_spaces,
    functionspace,
    assemble_scalar,
    dirichletbc,
    form,
    Expression,
    locate_dofs_geometrical,
    locate_dofs_topological,
)
from dolfinx.fem.petsc import (
    assemble_matrix,
    assemble_vector,
    apply_lifting,
    create_vector,
    set_bc,
)
from dolfinx.io import VTXWriter,XDMFFile
from dolfinx.mesh import create_rectangle, compute_midpoints, locate_entities_boundary, locate_entities, CellType, create_unit_square
from dolfinx.plot import vtk_mesh
from basix.ufl import element,  quadrature_element
from ufl import (
    FacetNormal,
    Identity,
    TestFunction,
    TrialFunction,
    div,
    curl,
    dot,
    ds,
    dx,
    inner,
    lhs,
    nabla_grad,
    rhs,
    sym,
    interpolate,
    SpatialCoordinate,
    as_vector,
    Measure,
    exp,
    sin,
    cos,
    pi,
    as_matrix
)
def l2_error(solution, exact_expr, space_type="E"):
    # Dx = dx(metadata={'quadrature_degree':1})
    if space_type == "E":
        # 计算误差表达式
        error_expr = form(inner(solution - exact_expr, solution - exact_expr) * dx)
    else:
        # 对于标量磁场
        error_expr = form((solution - exact_expr) * (solution - exact_expr) * dx)
    
    error = np.sqrt(MPI.COMM_WORLD.allreduce(assemble_scalar(error_expr), op=MPI.SUM))
    
    
    return error


def maxwell_simulation(N, dt, T=1.0, output_dir="results"):
    output_folder = Path(output_dir) / f"N{N}"
    output_folder.mkdir(exist_ok=True, parents=True)

    ax0 = 0;ay0 = 0
    bx0 = 1;by0 = 1

    Nx , Ny=N, N
    points = np.array([[ax0,ay0],[bx0,by0]], dtype=np.float64)
    n = [Nx, Ny]

    mesh = create_rectangle(MPI.COMM_WORLD, points, n, CellType.quadrilateral)
    V = element("N1curl", mesh.basix_cell(), 1)
    U = element("DG", mesh.basix_cell(), 0)
    Vs = functionspace(mesh, V)
    Us = functionspace(mesh, U)
    V_lagrange = functionspace(mesh, ("Lagrange", 1, (mesh.geometry.dim,)))
    U_lagrange = functionspace(mesh, ("Lagrange", 1))
    x = SpatialCoordinate(mesh)

    # 使用 ufl 定义表达式
    def curl_2d_E(E):
        return ufl.Dx(E[1], 0) - ufl.Dx(E[0], 1)

    # 精确解 - 使用 ufl 表达式
    def E_exact_expr(x, t):
        return as_vector([
        ufl.exp(-ufl.pi * t) * ufl.cos(ufl.pi * x[0]) * ufl.sin(ufl.pi * x[1]),
        -ufl.exp(-ufl.pi * t) * ufl.sin(ufl.pi * x[0]) * ufl.cos(ufl.pi * x[1])
    ])
    # 用于插值的 numpy 函数(需要传入时间参数)
    def E_exact_np(x, t):
        return np.array([
            np.exp(-np.pi * t) * np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]),
            -np.exp(-np.pi * t) * np.sin(np.pi * x[0]) * np.cos(np.pi * x[1])
        ])
    

    def H_exact_expr(x, t):
        return ufl.exp(-ufl.pi * t) * ufl.cos(ufl.pi * x[0]) * ufl.cos(ufl.pi * x[1])
    def H_exact_np(x, t):
        return np.exp(-np.pi * t) * np.cos(np.pi * x[0]) * np.cos(np.pi * x[1])
    

    def E_star_exact_expr(x, t):
        return as_vector([ufl.exp(-pi * t) * cos(pi * x[0]) * sin(pi * x[1]),
                          -ufl.exp(-pi * t) * sin(pi * x[0]) * cos(pi * x[1])])
    def E_star_exact_np(x, t):
        return np.array([
            np.exp(-np.pi * t) * np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]),
            -np.exp(-np.pi * t) * np.sin(np.pi * x[0]) * np.cos(np.pi * x[1])
        ])
    
    
    def K_exact_expr(x, t):
        return -(1 / ufl.pi) * ufl.exp(-ufl.pi * t) * ufl.cos(ufl.pi * x[0]) * ufl.cos(ufl.pi * x[1])
    def K_exact_np(x, t):
        return -(1 / np.pi) * np.exp(-np.pi * t) * np.cos(np.pi * x[0]) * np.cos(np.pi * x[1])
    
    
    def H_star_exact_np(x_val, t_val):
        return np.exp(-np.pi * t_val) * np.cos(np.pi * x_val[0]) * np.cos(np.pi * x_val[1])
    

    def f_exact_np(x, t):
        return np.array([
            np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]) * 
            ((np.sin(np.pi * x[1]))**2 - (np.sin(np.pi * x[0]))**2),
            np.sin(np.pi * x[0]) * np.cos(np.pi * x[1]) * 
            ((np.sin(np.pi * x[1]))**2 - (np.sin(np.pi * x[0]))**2)
        ])
    
    # 源项 hat_f
    def f_hat_expr(x,t):
        return as_vector([
            (ufl.exp(-pi * t) - 1) * ufl.cos(ufl.pi * x[0]) * ufl.sin(ufl.pi * x[1]) * 
            ((ufl.sin(ufl.pi * x[1]))**2 - (ufl.sin(pi * x[0]))**2),
            (ufl.exp(-pi * t) - 1) * sin(pi * x[0]) * cos(pi * x[1]) * 
            ((ufl.sin(pi * x[1]))**2 - (ufl.sin(ufl.pi * x[0]))**2)
        ])
    
    def f_hat_np(x, t):
        return np.array([
            (np.exp(-np.pi * t) - 1) * np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]) * 
            ((np.sin(np.pi * x[1]))**2 - (np.sin(np.pi * x[0]))**2),
            (np.exp(-np.pi * t) - 1) * np.sin(np.pi * x[0]) * np.cos(np.pi * x[1]) * 
            ((np.sin(np.pi * x[1]))**2 - (np.sin(np.pi * x[0]))**2)
        ])
    
    # 源项 g_z
    g_exact_expr = cos(pi * x[0]) * cos(pi * x[1]) * ((sin(pi * x[0]))**2 + (sin(pi * x[1]))**2)
    
    def g_exact_np(x, t):
        return np.cos(np.pi * x[0]) * np.cos(np.pi * x[1]) * ((np.sin(np.pi * x[0]))**2 + (np.sin(np.pi * x[1]))**2)
    
    # 源项 hat_g_z
    def g_hat_expr(t):
        return (ufl.exp(-pi * t) * cos(pi * x[0]) * cos(pi * x[1]) * 
                (((sin(pi * x[0]))**2 + (sin(pi * x[1]))**2) - 
                 (1 / pi) * (sin(pi * x[0]))**2 * sin(pi * x[1])**2) -
                cos(pi * x[0]) * cos(pi * x[1]) * ((sin(pi * x[0]))**2 + (sin(pi * x[1]))**2))
    
    def g_hat_np(x, t):
        return (np.exp(-np.pi * t) * np.cos(np.pi * x[0]) * np.cos(np.pi * x[1]) * 
                (((np.sin(np.pi * x[0]))**2 + (np.sin(np.pi * x[1]))**2) - 
                 (1 / np.pi) * (np.sin(np.pi * x[0]))**2 * (np.sin(np.pi * x[1]))**2) -
                np.cos(np.pi * x[0]) * np.cos(np.pi * x[1]) * 
                ((np.sin(np.pi * x[0]))**2 + (np.sin(np.pi * x[1]))**2))
    
    # 源项 g_z^*
    def g_star_expr(t):
        return -3 * pi * ufl.exp(-pi * t) * cos(pi * x[0]) * cos(pi * x[1])
    
    def g_star_np(x, t):
        return -3 * np.pi * np.exp(-np.pi * t) * np.cos(np.pi * x[0]) * np.cos(np.pi * x[1])
    
    # sigma 函数
    sigma_x_expr = (sin(pi * x[0]))**2
    sigma_y_expr = (sin(pi * x[1]))**2
    
    sigma_x = sigma_x_expr
    sigma_y = sigma_y_expr
    
    # 矩阵系数
    C_2d = as_matrix([[sigma_y, 0],
                      [0, sigma_x]])
    G_2d = as_matrix([[sigma_x, 0],
                      [0, sigma_y]])
    C_1d = sigma_x + sigma_y
    D_1d = sigma_x * sigma_y
    
    # PEC边界条件
    def pec_boundary(x):
        tol = 1e-10
        return (np.abs(x[0] - 0) < tol) | (np.abs(x[0] - 1) < tol) | \
            (np.abs(x[1] - 0) < tol) | (np.abs(x[1] - 1) < tol)

    boundary_facets = locate_entities_boundary(mesh, mesh.topology.dim - 1, pec_boundary)
    boundary_dofs_E = locate_dofs_topological(Vs, mesh.topology.dim - 1, boundary_facets)
    Ebc = dirichletbc(Function(Vs), boundary_dofs_E)
    Estarbc = dirichletbc(Function(Vs), boundary_dofs_E)

    
    # 初始值
    eps0 = 1.0  # ε0
    mu0 = 1.0   # μ0
    
    E_n = Function(Vs)
    E_n.interpolate(lambda x: E_exact_np(x, 0))
    
    E_starn = Function(Vs)
    E_starn.interpolate(lambda x: E_star_exact_np(x, 0))
    
    H_nhalf = Function(Us)
    H_nhalf.interpolate(lambda x: H_exact_np(x, 0.5 * dt))
    
    H_starn = Function(Us)
    H_starn.interpolate(lambda x: H_star_exact_np(x, 0.5 * dt))
    
    K_nhalf = Function(Us)
    K_nhalf.interpolate(lambda x: K_exact_np(x, 0.5 * dt))
    
    # 源项函数
    f_n = Function(Vs)
    f_n.interpolate(lambda x: f_exact_np(x, 0))
    
    f_hat = Function(Vs)
    f_hat.interpolate(lambda x: f_hat_np(x, 0.5 * dt))
    
    g_n = Function(Us)
    g_n.interpolate(lambda x: g_exact_np(x, 0))
    
    g_star = Function(Us)
    g_star.interpolate(lambda x: g_star_np(x, dt))
    
    g_hat = Function(Us)
    g_hat.interpolate(lambda x: g_hat_np(x, dt))

    #变分形式
    E_star = TrialFunction(Vs)
    psi = TestFunction(Vs)
    a1 = form(ufl.inner(E_star, psi) * dx )
    L1 = form(ufl.inner(E_starn, psi) * dx 
            + dt * ufl.inner(H_nhalf, curl(psi)) * dx)
    A1 = assemble_matrix(a1)
    A1.assemble()
    b1 = create_vector(Vs)
    E_starsolution = Function(Vs)

    E = TrialFunction(Vs)
    psi_hat = TestFunction(Vs)
    a2 = form(ufl.inner(E, psi_hat) * dx + 0.5 * dt * ufl.inner(C_2d * E, psi_hat) * dx )
    L2 = form(ufl.inner(E_n, psi_hat) * dx 
            - 0.5 * dt * ufl.inner(C_2d * E_n, psi_hat) * dx 
            + ufl.inner(E_starsolution, psi_hat) * dx 
            - ufl.inner(E_starn, psi_hat) * dx 
            + 0.5 * dt * ufl.inner(G_2d * E_starsolution, psi_hat) * dx 
            + 0.5 * dt * ufl.inner(G_2d * E_starn, psi_hat) * dx
            + dt * ufl.inner(f_n, psi_hat) * dx
            + dt * ufl.inner(f_hat, psi_hat) * dx)
    A2 = assemble_matrix(a2)
    A2.assemble()
    b2 = create_vector(Vs)
    Esolution = Function(Vs)

    H_star = TrialFunction(Us)
    phi = TestFunction(Us)
    a3 = form(ufl.inner(H_star, phi) * dx )
    L3 = form(ufl.inner(H_starn, phi) * dx
            - dt * ufl.inner(curl(Esolution), phi) * dx
            + dt * ufl.inner(g_star, phi) * dx)
    A3 = assemble_matrix(a3)
    A3.assemble()
    b3 = create_vector(Us)
    H_starsolution = Function(Us)

    H = TrialFunction(Us)
    phi_hat = TestFunction(Us)
    a4 = form(ufl.inner(H, phi_hat) * dx 
            + 0.5 * dt * ufl.inner(C_1d * H, phi_hat) * dx
            + 0.25 * (dt**2) * ufl.inner(D_1d * H, phi_hat) * dx)
    L4 = form(ufl.inner(H_nhalf, phi_hat) * dx 
            - 0.5 * dt * ufl.inner(C_1d * H_nhalf, phi_hat) * dx
            - dt * ufl.inner(D_1d * K_nhalf, phi_hat) * dx
            - 0.25 * (dt**2) * ufl.inner(D_1d * H_nhalf, phi_hat) * dx
            + ufl.inner(H_starsolution,phi_hat) * dx
            - ufl.inner(H_starn, phi_hat) * dx
            + dt * ufl.inner(g_n, phi_hat) * dx
            + dt * ufl.inner(g_hat, phi_hat) * dx)
    A4 = assemble_matrix(a4)
    A4.assemble()
    b4 = create_vector(Us)
    Hsolution = Function(Us)

    K = TrialFunction(Us)
    phi_wide = TestFunction(Us)
    a5 = form(ufl.inner(D_1d * K, phi_wide) * dx)
    L5 = form(ufl.inner(D_1d * K_nhalf, phi_wide) * dx
        + 0.5 * dt * ufl.inner(D_1d * Hsolution, phi_wide) * dx
        + 0.5 * dt * ufl.inner(D_1d * H_nhalf, phi_wide) * dx)
    A5 = assemble_matrix(a5)
    A5.assemble()
    b5 = create_vector(Us)
    Ksolution = Function(Us)

    solver1 = PETSc.KSP().create(mesh.comm)
    solver1.setOperators(A1)
    solver1.setType(PETSc.KSP.Type.PREONLY)
    pc1 = solver1.getPC()
    pc1.setType(PETSc.PC.Type.LU)

    solver2 = PETSc.KSP().create(mesh.comm)
    solver2.setOperators(A2)
    solver2.setType(PETSc.KSP.Type.PREONLY)
    pc2 = solver2.getPC()
    pc2.setType(PETSc.PC.Type.LU)

    solver3 = PETSc.KSP().create(mesh.comm)
    solver3.setOperators(A3)
    solver3.setType(PETSc.KSP.Type.PREONLY)
    pc3 = solver3.getPC()
    pc3.setType(PETSc.PC.Type.LU)

    solver4 = PETSc.KSP().create(mesh.comm)
    solver4.setOperators(A4)
    solver4.setType(PETSc.KSP.Type.PREONLY)
    pc4 = solver4.getPC()
    pc4.setType(PETSc.PC.Type.LU)

    solver5 = PETSc.KSP().create(mesh.comm)
    solver5.setOperators(A5)
    solver5.setType(PETSc.KSP.Type.PREONLY)
    pc5 = solver5.getPC()
    pc5.setType(PETSc.PC.Type.LU)
 
    
    E_for_output = Function(V_lagrange)
    H_for_output = Function(U_lagrange)
    folder = output_folder
    # 写入网格文件
    with XDMFFile(mesh.comm, folder / "mesh.xdmf", "w") as xdmf_file:
        xdmf_file.write_mesh(mesh)

    E_for_output = Function(V_lagrange)

    xdmf_E = XDMFFile(mesh.comm, folder / "Esolution_over_time.xdmf", "w")
    xdmf_H = XDMFFile(mesh.comm, folder / "Hsolution_over_time.xdmf", "w")
    xdmf_E.write_mesh(mesh)
    xdmf_H.write_mesh(mesh)
        
    t_E = 0
    t_half = 0.5 * dt
    Nt = 1000

    for n in range(Nt):
        t_E_new = t_E + dt
        t_next_half = t_half + dt

        with b1.localForm() as loc_1:
            loc_1.set(0)
            assemble_vector(b1, L1)
            apply_lifting(b1, [a1], [[Estarbc]])
            b1.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
            set_bc(b1, [Estarbc])
            solver1.solve(b1, E_starsolution.x.petsc_vec)
            E_starsolution.x.scatter_forward()

        f_hat.interpolate(lambda x: np.array([
            (np.exp(-pi * t_half) - 1) * np.cos(np.pi * x[0]) * np.sin(np.pi * x[1]) * ((np.sin(np.pi * x[1]))**2 - (np.sin(np.pi * x[0]))**2),
            (np.exp(-pi * t_half) - 1) * np.sin(np.pi * x[0]) * np.cos(np.pi * x[1]) * ((np.sin(np.pi * x[1]))**2 - (np.sin(np.pi * x[0]))**2)
            ]))

        with b2.localForm() as loc_2:
            loc_2.set(0)
            assemble_vector(b2, L2)
            apply_lifting(b2, [a2], [[Ebc]])
            b2.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
            set_bc(b2, [Ebc])
            solver2.solve(b2, Esolution.x.petsc_vec)
            Esolution.x.scatter_forward()

        g_star.interpolate(lambda x: g_star_np(x, t_E_new))

        with b3.localForm() as loc_3:
            loc_3.set(0)
        assemble_vector(b3, L3)
        apply_lifting(b3, [a3], [[]])
        b3.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
        set_bc(b3, [])
        solver3.solve(b3, H_starsolution.x.petsc_vec)
        H_starsolution.x.scatter_forward()

        g_hat.interpolate(lambda x: g_hat_np(x, t_E_new))

        with b4.localForm() as loc_4:
            loc_4.set(0)
        assemble_vector(b4, L4)
        apply_lifting(b4, [a4], [[]])
        b4.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
        set_bc(b4, [])
        solver4.solve(b4, Hsolution.x.petsc_vec)
        Hsolution.x.scatter_forward()

        with b5.localForm() as loc_5:
            loc_5.set(0)
        assemble_vector(b5, L5)
        b5.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
        set_bc(b5, [])
        solver5.solve(b5, Ksolution.x.petsc_vec)
        Ksolution.x.scatter_forward()

        # 每100步或最后一步保存XDMF文件
        if n % 100 == 0 or n == Nt - 1:
            E_for_output.interpolate(Esolution)
            H_for_output.interpolate(Hsolution)
            E_for_output.name = "E_field"
            # Hsolution.name = "H_field"
            H_for_output.name = "H_field"

            # Hsolution.name = "H_field"
            xdmf_E.write_function(E_for_output, t_E_new)
            xdmf_H.write_function(H_for_output, t_next_half)
            # vtk_E.write_function(E_for_output, t_E_new)
            # vtk_H.write_function(Hsolution, t_next_half)
            print(f"  -> Data written at t={t_E_new:.6e} s (={t_E_new*1e9:.3f} ns)")
       
        H_nhalf.x.array[:] = Hsolution.x.array[:]
        E_n.x.array[:] = Esolution.x.array[:]
        H_starn.x.array[:] = H_starsolution.x.array[:]
        E_starn.x.array[:] = E_starsolution.x.array[:]
        K_nhalf.x.array[:] = Ksolution.x.array[:]
        E_n.x.scatter_forward()
        H_nhalf.x.scatter_forward()
        H_starn.x.scatter_forward()
        E_starn.x.scatter_forward()
        K_nhalf.x.scatter_forward()

        t_E = t_E_new
        t_half = t_next_half

    

    # 计算最终时刻误差
    E_expr_final = E_exact_expr(x, t_E)
    H_expr_final = H_exact_expr(x, t_half)
    
    final_E_error = l2_error(Esolution, E_expr_final, space_type="E")
    final_H_error = l2_error(Hsolution, H_expr_final, space_type="H")

    xdmf_E.close()
    xdmf_H.close()
    b1.destroy()
    b2.destroy()
    b3.destroy()
    b4.destroy()
    b5.destroy()
    solver1.destroy()
    solver2.destroy()
    solver3.destroy()
    solver4.destroy()
    
    

    h = 1.0/N  # 网格尺寸
    return {
        'N': N,
        'h': h,
        'dt': dt,
        'final_time_E': t_E,
        'final_time_H': t_half,
        'error_E_abs': final_E_error,
        'error_H_abs': final_H_error,
        'num_time_steps': Nt
    }

def calculate_convergence_rates(results):
    
    print("空间收敛性分析")
   
    # 提取数据
    N_values = [r['N'] for r in results]
    h_values = [r['h'] for r in results]
    # dt_values = [r['dt'] for r in results]
    E_errors = [r['error_E_abs'] for r in results]
    H_errors = [r['error_H_abs'] for r in results]
    
    print(f"{'N':>6} {'h':>10} {'E_err(绝对)':>15} {'H_err(绝对)':>15} ")
    print("-" * 85)
    
    for i in range(len(results)):
        print(f"{N_values[i]:6d} {h_values[i]:10.6f} "
              f"{E_errors[i]:15.6e} {H_errors[i]:15.6e} ")#{dt_values[i]:12.6e}
    
    # 计算收敛阶
    print("\n收敛阶计算:")
    print("-" * 50)
    
    convergence_rates = []
    for i in range(1, len(h_values)):
        rate_E = np.log(E_errors[i-1] / E_errors[i]) / np.log(h_values[i-1] / h_values[i])
        rate_H = np.log(H_errors[i-1] / H_errors[i]) / np.log(h_values[i-1] / h_values[i])
        print(f"h: {h_values[i-1]:.4f} -> {h_values[i]:.4f} (细化比: {h_values[i-1]/h_values[i]:.2f})")
        print(f"  E场绝对误差收敛阶: {rate_E:.4f}")
        print(f"  H场绝对误差收敛阶: {rate_H:.4f}")
    
    return {
        'N_values': N_values,
        'h_values': h_values,
        'E_errors': E_errors,
        'H_errors': H_errors,
        'convergence_rates': convergence_rates
    }

def spatial_convergence_test():
 
    print("="*70)
    print("开始空间收敛性测试")
    print("="*70)
    
    
    T = 0.01   # 总时间
    dt = 1e-5# 固定时间步长
    N_values = [10, 20, 40, 80, 160]
    results = []
    
    for N in N_values:
        h = 1/N
        # dt = 0.01 * h
        print(f"\n运行 N={N}, h={1.0/N:.4f}, dt={dt:.6e}, T={T}")
        print("-" * 40)
        
        result = maxwell_simulation(
            N=N,
            dt=dt,
            T=T,
            output_dir=f"cr2"
        )
        
        results.append(result)
        
        print(f"完成! 最终误差:")
        print(f"  E场绝对误差: {result['error_E_abs']:.6e}")
        print(f"  H场绝对误差: {result['error_H_abs']:.6e}")
    
    # 计算收敛阶
    convergence_data = calculate_convergence_rates(results)
    return {
        'results': results,
        'convergence_data': convergence_data
    }

if __name__ == "__main__":
    results = spatial_convergence_test()

Here are my results.


Estimated Convergence Rates
Refinement	E field order	H field order
0.1000 → 0.0500	1.4940	0.5751
0.0500 → 0.0250	1.3871	0.6573
0.0250 → 0.0125	1.0953	0.9593
0.0125 → 0.0063	0.9845	1.2414

This code is simply to lengthy for me to parse in detail.
Would you mind:

  1. Reducing it to solving just one of the four equation above, and use manufactured terms for all other terms (that in your code stems from solving the remaining 3 PDEs).
  2. Ensure that your finite element pair is stable for the discretization.
  3. It is unclear where your custom L2-norm enters, as there is no such thing in the code (that I can see at a first glance). If you mean by setting H in DG-0, that is not changing the norm (per say). If you want custom interpolation in the norm, you should pass the custom integration point (or set a low order quadrature degree) in the integration measure dx.

Sorry, I didn’t understand your meaning. The two-dimensional time-domain Maxwell equations with Cohen Monk PML consist of five equations. At the end of the time loop, I called the defined L² norm to calculate the errors of the electric and magnetic fields at the final time for convergence order analysis. This is my minimal code:

def maxwell_simulation(N, dt, T=0.01):
    domain = create_rectangle(MPI.COMM_WORLD, [[0,0], [1,1]], [N, N], CellType.quadrilateral)
    x = SpatialCoordinate(domain)
    
    V = functionspace(domain, ("N1curl", 1))
    U = functionspace(domain, ("DG", 0))
    
    sigma_x = (sin(pi * x[0]))**2
    sigma_y = (sin(pi * x[1]))**2
    C_2d = as_matrix([[sigma_y, 0], [0, sigma_x]])
    G_2d = as_matrix([[sigma_x, 0], [0, sigma_y]])
    C_1d = sigma_x + sigma_y
    D_1d = sigma_x * sigma_y
    
    def pec_boundary(x):
        tol = 1e-10
        return (np.abs(x[0]-0)<tol) | (np.abs(x[0]-1)<tol) | (np.abs(x[1]-0)<tol) | (np.abs(x[1]-1)<tol)
    
    boundary_facets = locate_entities_boundary(domain, domain.topology.dim-1, pec_boundary)
    boundary_dofs = fem.locate_dofs_topological(V, domain.topology.dim-1, boundary_facets)
    bc = dirichletbc(Function(V), boundary_dofs)
    
    E_star, E = TrialFunction(V), TrialFunction(V)
    psi, psi_hat = TestFunction(V), TestFunction(V)
    
    H_star, H = TrialFunction(U), TrialFunction(U)
    phi, phi_hat = TestFunction(U), TestFunction(U)
    
    K = TrialFunction(U)
    phi_wide = TestFunction(U)
    
    a1 = form(inner(E_star, psi) * dx)
    L1 = form(inner(Function(V), psi) * dx + dt * inner(Function(U), curl(psi)) * dx)
    
    a2 = form(inner(E, psi_hat) * dx + 0.5 * dt * inner(C_2d * E, psi_hat) * dx)
    L2 = form(inner(Function(V), psi_hat) * dx - 0.5 * dt * inner(C_2d * Function(V), psi_hat) * dx
            + inner(Function(V), psi_hat) * dx - inner(Function(V), psi_hat) * dx
            + 0.5 * dt * inner(G_2d * Function(V), psi_hat) * dx + 0.5 * dt * inner(G_2d * Function(V), psi_hat) * dx
            + dt * inner(Function(V), psi_hat) * dx + dt * inner(Function(V), psi_hat) * dx)
    
    a3 = form(inner(H_star, phi) * dx)
    L3 = form(inner(Function(U), phi) * dx - dt * inner(curl(Function(V)), phi) * dx
            + dt * inner(Function(U), phi) * dx)
    
    a4 = form(inner(H, phi_hat) * dx + 0.5 * dt * inner(C_1d * H, phi_hat) * dx
            + 0.25 * (dt**2) * inner(D_1d * H, phi_hat) * dx)
    L4 = form(inner(Function(U), phi_hat) * dx - 0.5 * dt * inner(C_1d * Function(U), phi_hat) * dx
            - dt * inner(D_1d * Function(U), phi_hat) * dx - 0.25 * (dt**2) * inner(D_1d * Function(U), phi_hat) * dx
            + inner(Function(U), phi_hat) * dx - inner(Function(U), phi_hat) * dx
            + dt * inner(Function(U), phi_hat) * dx + dt * inner(Function(U), phi_hat) * dx)
    
    a5 = form(inner(D_1d * K, phi_wide) * dx)
    L5 = form(inner(D_1d * Function(U), phi_wide) * dx + 0.5 * dt * inner(D_1d * Function(U), phi_wide) * dx
            + 0.5 * dt * inner(D_1d * Function(U), phi_wide) * dx)
    
    A1, A2, A3, A4, A5 = [assemble_matrix(a) for a in [a1, a2, a3, a4, a5]]
    [A.assemble() for A in [A1, A2, A3, A4, A5]]
    
    b1, b2, b3, b4, b5 = [create_vector(V if i<2 else U) for i in range(5)]
    solutions = [Function(V if i<2 else U) for i in range(5)]
    
    solvers = []
    for A in [A1, A2, A3, A4, A5]:
        solver = PETSc.KSP().create(domain.comm)
        solver.setOperators(A)
        solver.setType(PETSc.KSP.Type.PREONLY)
        solver.getPC().setType(PETSc.PC.Type.LU)
        solvers.append(solver)
    
    Nt = int(T/dt)
    for n in range(Nt):
        for i, (A, b, L, solver, sol, bc_list) in enumerate([
            (A1, b1, L1, solvers[0], solutions[0], [bc]),
            (A2, b2, L2, solvers[1], solutions[1], [bc]),
            (A3, b3, L3, solvers[2], solutions[2], []),
            (A4, b4, L4, solvers[3], solutions[3], []),
            (A5, b5, L5, solvers[4], solutions[4], [])
        ]):
            with b.localForm() as loc:
                loc.set(0)
            assemble_vector(b, L)
            apply_lifting(b, [A], [bc_list])
            b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
            set_bc(b, bc_list)
            solver.solve(b, sol.x.petsc_vec)
            sol.x.scatter_forward()
    
    return {'N': N, 'h': 1.0/N, 'dt': dt}

def spatial_convergence_test():
    dt = 1e-5
    N_values = [10, 20, 40, 80, 160]
    results = []
    
    for N in N_values:
        result = maxwell_simulation(N=N, dt=dt)
        results.append(result)
    
    return results

if __name__ == "__main__":
    spatial_convergence_test()

And I’ve tried custom integration rules,

dx_center = ufl.Measure(
#         "dx",
#         domain=mesh,
#         metadata={
#             "quadrature_scheme": "custom",
#             "quadrature_points": points,
#             "quadrature_weights": weights,
#         }
#     )

but the results appear to be the same as when I simply specify an integration precision of 1 like below:

Dx = dx(metadata={'quadrature_degree':1})

The equations I’m solving are as follows:

\left(\sigma_{1}\sigma_{2}\,\frac{K_{h}^{n+\frac{3}{2}}-K_{h}^{n+\frac{1}{2}}}{\tau},\widetilde{\Phi}_{h}\right) =\left(\sigma_{1}\sigma_{2}\,\frac{H_{h}^{n+\frac{3}{2}}+H_{h}^{n+\frac{1}{2}}}{2},\widetilde{\Phi}_{h}\right), \quad\forall\widetilde{\Phi}_{h}\in U_{h}, \tag{1}
\left(\epsilon_{0}\frac{E_{h}^{*n+1}-E_{h}^{*n}}{\tau},\Psi_{h}\right) =\left(H_{h}^{n+\frac{1}{2}},\nabla\times\Psi_{h}\right), \quad\forall\Psi_{h}\in V_{h}^{0}, \tag{2}
\left(\mu_{0}\frac{H_{h}^{*n+\frac{3}{2}}-H_{h}^{*n+\frac{1}{2}}}{\tau},\Phi_{h}\right) =-\left(\nabla\times E_{h}^{n+1},\Phi_{h}\right) +\left(g_{z}^{*}\left(t_{n+1}\right),\Phi_{h}\right), \quad\forall\Phi_{h}\in U_{h}, \tag{3}
\left(\frac{E_{h}^{n+1}-E_{h}^{n}}{\tau},\widehat{\Psi}_{h}\right) +\left(C_{2d}\,\frac{E_{h}^{n+1}+E_{h}^{n}}{2},\widehat{\Psi}_{h}\right)\\ = \left(\frac{E_{h}^{*n+1}-E_{h}^{*n}}{\tau},\widehat{\Psi}_{h}\right) +\left(G_{2d}\frac{E_{h}^{*n+1}+E_{h}^{*n}}{2},\widehat{\Psi}_{h}\right) +\left((f+\hat{f})(t_{n+\frac{1}{2}}),\widehat{\Psi}_{h}\right), \quad\forall\widehat{\Psi}_{h}\in V_{h}^{0}, \tag{4}
\left(\frac{H_{h}^{n+\frac{3}{2}}-H_{h}^{n+\frac{1}{2}}}{\tau},\widehat{\Phi}_{h}\right) +\left((\sigma_{1}+\sigma_{2})\frac{H_{h}^{n+\frac{3}{2}}+H_{h}^{n+\frac{1}{2}}}{2},\widehat{\Phi}_{h}\right)+\left(\sigma_{1}\sigma_{2}\frac{K_{h}^{n+\frac{3}{2}}+K_{h}^{n+\frac{1}{2}}}{2},\widehat{\Phi}_{h}\right)\\ =\left(\frac{H_{h}^{*n+\frac{3}{2}}-H_{h}^{*n+\frac{1}{2}}}{\tau},\widehat{\Phi}_{h}\right) +\left((g_{z}+\hat{g}_{z})\left(t_{n+1}\right),\widehat{\Phi}_{h}\right), \quad\forall\widehat{\Phi}_{h}\in U_{h}. \tag{5}

My point is that since you have a manufactured solution, you can reduce say the first system

F(K^{n+\frac{3}{2}}, K^{n+\frac{1}{2}}, H^{n+\frac{3}{2}}, H^{n+\frac{1}{2}}, \tilde \phi_h)=0
to
F(K^{n+\frac{3}{2}}, K_{ex}^{n+\frac{1}{2}}, H_{ex}^{n+\frac{3}{2}}, H_{ex}^{n+\frac{1}{2}}, \tilde \phi_h)=0

where the _{ex} subscript indicates that you have inserted the manufactured/exact solution for K and H. If you solve that for a single time step, do you observe the expected convergence in K?

Once it converges for a single time step, check convergence for multiple steps.

Then, step by step you can replace the components of your segregated solver (splitting scheme) with each of these steps.

As I am not familiar with the particular splitting scheme that you are using to solve these equations, its hard for me to comment on the overall observed behavior.

Thank you for your reply. I will make another attempt to revise it.